From 12f357fc403023276df99b081972fc9ad047c164 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 14 Jul 2025 00:09:44 -0700 Subject: [PATCH 001/102] First commit of the new "simple engine" tutorial. This is a VERY early commit to start getting proofreading while I still iterate on getting the engine to work. Engine still needs assets. Engine still needs Android project created. Engine currently crashes on running. Add `SimpleEngine` with Entity-Component-System (ECS) implementation - Introduced the `SimpleEngine` project featuring a modular ECS-based architecture. - Implemented core files: `Entity`, `Component`, and `TransformComponent` for managing entities and transformations. - Added CMake setup with Vulkan, GLFW, GLM, TinyGLTF, and KTX dependencies for rendering support. - Integrated shader compilation workflow using `slangc`. - Included initial scene setup with basic camera and cube entities in `main.cpp`. --- README.adoc | 2 + attachments/simple_engine/CMakeLists.txt | 198 + attachments/simple_engine/audio_system.cpp | 453 + attachments/simple_engine/audio_system.h | 236 + .../simple_engine/camera_component.cpp | 63 + attachments/simple_engine/camera_component.h | 184 + attachments/simple_engine/component.cpp | 11 + attachments/simple_engine/component.h | 84 + attachments/simple_engine/crash_reporter.h | 291 + attachments/simple_engine/debug_system.h | 288 + .../simple_engine/descriptor_manager.cpp | 224 + .../simple_engine/descriptor_manager.h | 117 + attachments/simple_engine/engine.cpp | 393 + attachments/simple_engine/engine.h | 208 + attachments/simple_engine/entity.cpp | 30 + attachments/simple_engine/entity.h | 160 + attachments/simple_engine/imgui/imconfig.h | 51 + attachments/simple_engine/imgui/imgui.cpp | 13278 ++++++++++++++++ attachments/simple_engine/imgui/imgui.h | 1787 +++ .../simple_engine/imgui/imgui_draw.cpp | 2943 ++++ .../simple_engine/imgui/imgui_internal.h | 1157 ++ .../simple_engine/imgui/stb_rect_pack.h | 573 + .../simple_engine/imgui/stb_textedit.h | 1317 ++ .../simple_engine/imgui/stb_truetype.h | 3263 ++++ attachments/simple_engine/imgui_system.cpp | 730 + attachments/simple_engine/imgui_system.h | 182 + attachments/simple_engine/main.cpp | 138 + attachments/simple_engine/mesh_component.cpp | 78 + attachments/simple_engine/mesh_component.h | 146 + attachments/simple_engine/model_loader.cpp | 317 + attachments/simple_engine/model_loader.h | 118 + attachments/simple_engine/physics_system.cpp | 1151 ++ attachments/simple_engine/physics_system.h | 339 + attachments/simple_engine/pipeline.cpp | 632 + attachments/simple_engine/pipeline.h | 162 + attachments/simple_engine/platform.cpp | 501 + attachments/simple_engine/platform.h | 441 + attachments/simple_engine/renderer.h | 398 + .../simple_engine/renderer_compute.cpp | 225 + attachments/simple_engine/renderer_core.cpp | 495 + .../simple_engine/renderer_pipelines.cpp | 485 + .../simple_engine/renderer_rendering.cpp | 446 + .../simple_engine/renderer_resources.cpp | 725 + attachments/simple_engine/renderer_utils.cpp | 269 + .../simple_engine/resource_manager.cpp | 26 + attachments/simple_engine/resource_manager.h | 252 + attachments/simple_engine/shaders/hrtf.slang | 126 + attachments/simple_engine/shaders/imgui.slang | 51 + .../simple_engine/shaders/lighting.slang | 95 + attachments/simple_engine/shaders/pbr.slang | 199 + .../simple_engine/shaders/physics.slang | 319 + .../simple_engine/shaders/texturedMesh.slang | 52 + attachments/simple_engine/swap_chain.h | 103 + .../simple_engine/transform_component.cpp | 28 + .../simple_engine/transform_component.h | 131 + attachments/simple_engine/vulkan_device.cpp | 288 + attachments/simple_engine/vulkan_device.h | 154 + attachments/simple_engine/vulkan_dispatch.cpp | 13 + .../01_introduction.adoc | 39 + .../02_math_foundations.adoc | 1080 ++ .../03_camera_implementation.adoc | 434 + .../04_transformation_matrices.adoc | 182 + .../05_vulkan_integration.adoc | 263 + .../Camera_Transformations/06_conclusion.adoc | 62 + .../Camera_Transformations/index.adoc | 21 + .../Engine_Architecture/01_introduction.adoc | 62 + .../02_architectural_patterns.adoc | 325 + .../03_component_systems.adoc | 554 + .../04_resource_management.adoc | 685 + .../05_rendering_pipeline.adoc | 695 + .../Engine_Architecture/06_event_systems.adoc | 542 + .../Engine_Architecture/conclusion.adoc | 66 + .../GUI/01_introduction.adoc | 43 + .../GUI/02_imgui_setup.adoc | 696 + .../GUI/03_input_handling.adoc | 503 + .../GUI/04_ui_elements.adoc | 602 + .../GUI/05_vulkan_integration.adoc | 854 + .../GUI/06_conclusion.adoc | 132 + en/Building_a_Simple_Engine/GUI/index.adoc | 22 + .../Lighting_Materials/01_introduction.adoc | 176 + .../02_lighting_models.adoc | 187 + .../Lighting_Materials/03_push_constants.adoc | 143 + .../04_lighting_implementation.adoc | 509 + .../05_vulkan_integration.adoc | 421 + .../Lighting_Materials/06_conclusion.adoc | 51 + .../Lighting_Materials/index.adoc | 21 + .../Loading_Models/01_introduction.adoc | 40 + .../Loading_Models/02_project_setup.adoc | 357 + .../Loading_Models/03_model_system.adoc | 396 + .../Loading_Models/04_loading_gltf.adoc | 760 + .../Loading_Models/05_pbr_rendering.adoc | 576 + .../Loading_Models/06_multiple_objects.adoc | 786 + .../Loading_Models/07_scene_rendering.adoc | 628 + .../Loading_Models/08_animations.adoc | 1023 ++ .../Loading_Models/09_conclusion.adoc | 35 + .../Loading_Models/index.adoc | 30 + .../Mobile_Development/01_introduction.adoc | 40 + .../02_platform_considerations.adoc | 199 + .../03_performance_optimizations.adoc | 359 + .../04_rendering_approaches.adoc | 304 + .../05_vulkan_extensions.adoc | 326 + .../Mobile_Development/06_conclusion.adoc | 276 + .../Mobile_Development/index.adoc | 21 + .../Subsystems/01_introduction.adoc | 56 + .../Subsystems/02_audio_basics.adoc | 243 + .../Subsystems/03_vulkan_audio.adoc | 442 + .../Subsystems/04_physics_basics.adoc | 564 + .../Subsystems/05_vulkan_physics.adoc | 773 + .../Subsystems/06_conclusion.adoc | 118 + .../Subsystems/index.adoc | 21 + .../Tooling/01_introduction.adoc | 40 + .../Tooling/02_cicd.adoc | 246 + .../Tooling/03_debugging_and_renderdoc.adoc | 369 + .../Tooling/04_crash_minidump.adoc | 499 + .../Tooling/05_extensions.adoc | 314 + .../06_packaging_and_distribution.adoc | 548 + .../Tooling/07_conclusion.adoc | 239 + .../Tooling/index.adoc | 22 + en/Building_a_Simple_Engine/introduction.adoc | 62 + en/conclusion.adoc | 72 + 120 files changed, 57280 insertions(+) create mode 100644 attachments/simple_engine/CMakeLists.txt create mode 100644 attachments/simple_engine/audio_system.cpp create mode 100644 attachments/simple_engine/audio_system.h create mode 100644 attachments/simple_engine/camera_component.cpp create mode 100644 attachments/simple_engine/camera_component.h create mode 100644 attachments/simple_engine/component.cpp create mode 100644 attachments/simple_engine/component.h create mode 100644 attachments/simple_engine/crash_reporter.h create mode 100644 attachments/simple_engine/debug_system.h create mode 100644 attachments/simple_engine/descriptor_manager.cpp create mode 100644 attachments/simple_engine/descriptor_manager.h create mode 100644 attachments/simple_engine/engine.cpp create mode 100644 attachments/simple_engine/engine.h create mode 100644 attachments/simple_engine/entity.cpp create mode 100644 attachments/simple_engine/entity.h create mode 100644 attachments/simple_engine/imgui/imconfig.h create mode 100644 attachments/simple_engine/imgui/imgui.cpp create mode 100644 attachments/simple_engine/imgui/imgui.h create mode 100644 attachments/simple_engine/imgui/imgui_draw.cpp create mode 100644 attachments/simple_engine/imgui/imgui_internal.h create mode 100644 attachments/simple_engine/imgui/stb_rect_pack.h create mode 100644 attachments/simple_engine/imgui/stb_textedit.h create mode 100644 attachments/simple_engine/imgui/stb_truetype.h create mode 100644 attachments/simple_engine/imgui_system.cpp create mode 100644 attachments/simple_engine/imgui_system.h create mode 100644 attachments/simple_engine/main.cpp create mode 100644 attachments/simple_engine/mesh_component.cpp create mode 100644 attachments/simple_engine/mesh_component.h create mode 100644 attachments/simple_engine/model_loader.cpp create mode 100644 attachments/simple_engine/model_loader.h create mode 100644 attachments/simple_engine/physics_system.cpp create mode 100644 attachments/simple_engine/physics_system.h create mode 100644 attachments/simple_engine/pipeline.cpp create mode 100644 attachments/simple_engine/pipeline.h create mode 100644 attachments/simple_engine/platform.cpp create mode 100644 attachments/simple_engine/platform.h create mode 100644 attachments/simple_engine/renderer.h create mode 100644 attachments/simple_engine/renderer_compute.cpp create mode 100644 attachments/simple_engine/renderer_core.cpp create mode 100644 attachments/simple_engine/renderer_pipelines.cpp create mode 100644 attachments/simple_engine/renderer_rendering.cpp create mode 100644 attachments/simple_engine/renderer_resources.cpp create mode 100644 attachments/simple_engine/renderer_utils.cpp create mode 100644 attachments/simple_engine/resource_manager.cpp create mode 100644 attachments/simple_engine/resource_manager.h create mode 100644 attachments/simple_engine/shaders/hrtf.slang create mode 100644 attachments/simple_engine/shaders/imgui.slang create mode 100644 attachments/simple_engine/shaders/lighting.slang create mode 100644 attachments/simple_engine/shaders/pbr.slang create mode 100644 attachments/simple_engine/shaders/physics.slang create mode 100644 attachments/simple_engine/shaders/texturedMesh.slang create mode 100644 attachments/simple_engine/swap_chain.h create mode 100644 attachments/simple_engine/transform_component.cpp create mode 100644 attachments/simple_engine/transform_component.h create mode 100644 attachments/simple_engine/vulkan_device.cpp create mode 100644 attachments/simple_engine/vulkan_device.h create mode 100644 attachments/simple_engine/vulkan_dispatch.cpp create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/index.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc create mode 100644 en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/03_input_handling.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/06_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/GUI/index.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/index.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/Loading_Models/index.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/Mobile_Development/index.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/Subsystems/index.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/02_cicd.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/05_extensions.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc create mode 100644 en/Building_a_Simple_Engine/Tooling/index.adoc create mode 100644 en/Building_a_Simple_Engine/introduction.adoc create mode 100644 en/conclusion.adoc diff --git a/README.adoc b/README.adoc index 3f2cdff9..4b688e06 100644 --- a/README.adoc +++ b/README.adoc @@ -26,6 +26,8 @@ It also contains Vulkan usage clarifications, improved synchronization and new c The repository is organized into several important directories: * `en/` - Contains the tutorial content in English, organized by chapters +** The main tutorial covers fundamental Vulkan concepts (chapters 00-17) +** The "Building a Simple Engine" section builds upon these fundamentals to create a structured rendering engine * `attachments/` - Contains code examples, shader files, and resources used in the tutorial * `images/` - Contains illustrations, diagrams, and screenshots used in the tutorial * `scripts/` - Contains utility scripts, including dependency installation scripts diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt new file mode 100644 index 00000000..56c86f6b --- /dev/null +++ b/attachments/simple_engine/CMakeLists.txt @@ -0,0 +1,198 @@ +cmake_minimum_required(VERSION 3.29) + +# Enable C++ module dependency scanning +set(CMAKE_CXX_SCAN_FOR_MODULES ON) + +project(SimpleEngine VERSION 1.0.0 LANGUAGES CXX C) + +# Add CMake module path for custom find modules +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../CMake") + +# Find required packages +find_package (glfw3 REQUIRED) +find_package (glm REQUIRED) +find_package (Vulkan REQUIRED) +find_package (TinyGLTF REQUIRED) +find_package (KTX REQUIRED) +find_package (stb REQUIRED) + +# set up Vulkan C++ module +add_library(VulkanCppModule) +add_library(Vulkan::cppm ALIAS VulkanCppModule) + +target_compile_definitions(VulkanCppModule + PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1 +) +target_include_directories(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}" +) +target_link_libraries(VulkanCppModule + PUBLIC + Vulkan::Vulkan +) + +set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) + +target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${Vulkan_INCLUDE_DIR}" + FILES + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" +) + + +# Add the vulkan.cppm file directly as a source file +target_sources(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" +) + +# Platform-specific settings +if(ANDROID) + # Android-specific settings + add_definitions(-DPLATFORM_ANDROID=1) + add_definitions(-DPLATFORM_DESKTOP=0) +else() + # Desktop-specific settings + add_definitions(-DPLATFORM_ANDROID=0) + add_definitions(-DPLATFORM_DESKTOP=1) +endif() + +# Shader compilation +# Find Slang shaders +file(GLOB SLANG_SHADER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.slang) + +# Find slangc executable +find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin REQUIRED) + +# Compile Slang shaders using slangc +foreach(SHADER ${SLANG_SHADER_SOURCES}) + get_filename_component(SHADER_NAME ${SHADER} NAME) + get_filename_component(SHADER_NAME_WE ${SHADER_NAME} NAME_WE) + string(REGEX REPLACE "\.slang$" "" OUTPUT_NAME ${SHADER_NAME}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + COMMAND ${SLANGC_EXECUTABLE} ${SHADER} -target spirv -profile spirv_1_4 -emit-spirv-directly -o ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + DEPENDS ${SHADER} + COMMENT "Compiling Slang shader ${SHADER_NAME} with slangc" + ) + list(APPEND SHADER_SPVS ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv) +endforeach() + +add_custom_target(shaders DEPENDS ${SHADER_SPVS}) + +# Source files +set(SOURCES + main.cpp + engine.cpp + platform.cpp + renderer_core.cpp + renderer_rendering.cpp + renderer_pipelines.cpp + renderer_compute.cpp + renderer_utils.cpp + renderer_resources.cpp + resource_manager.cpp + entity.cpp + component.cpp + transform_component.cpp + mesh_component.cpp + camera_component.cpp + model_loader.cpp + audio_system.cpp + physics_system.cpp + imgui_system.cpp + imgui/imgui.cpp + imgui/imgui_draw.cpp + vulkan_dispatch.cpp + vulkan_device.cpp + pipeline.cpp + descriptor_manager.cpp +) + +# Create executable +add_executable(SimpleEngine ${SOURCES}) +add_dependencies(SimpleEngine shaders) +set_target_properties (SimpleEngine PROPERTIES CXX_STANDARD 20) + +# Link libraries +target_link_libraries(SimpleEngine PRIVATE + Vulkan::cppm + glm::glm + tinygltf::tinygltf + KTX::ktx + stb::stb +) + +if(NOT DEFINED ANDROID) + target_link_libraries(SimpleEngine PRIVATE glfw) +endif() + +# Copy model and texture files if they exist +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/models) + add_custom_command(TARGET SimpleEngine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/models + COMMENT "Copying models to output directory" + ) +endif() + +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/textures) + add_custom_command(TARGET SimpleEngine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/textures ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/textures + COMMENT "Copying textures to output directory" + ) +endif() + +# Add packaging configuration +include(CPack) + +# Set package properties +set(CPACK_PACKAGE_NAME "SimpleEngine") +set(CPACK_PACKAGE_VENDOR "SimpleEngine Team") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple game engine built with Vulkan") +set(CPACK_PACKAGE_VERSION "1.0.0") +set(CPACK_PACKAGE_VERSION_MAJOR "1") +set(CPACK_PACKAGE_VERSION_MINOR "0") +set(CPACK_PACKAGE_VERSION_PATCH "0") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "SimpleEngine") + +# Set platform-specific package generators +if(WIN32) + set(CPACK_GENERATOR "ZIP;NSIS") + set(CPACK_NSIS_PACKAGE_NAME "SimpleEngine") + set(CPACK_NSIS_DISPLAY_NAME "SimpleEngine") + set(CPACK_NSIS_HELP_LINK "https://github.com/yourusername/SimpleEngine") + set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/yourusername/SimpleEngine") + set(CPACK_NSIS_CONTACT "your.email@example.com") + set(CPACK_NSIS_MODIFY_PATH ON) +elseif(APPLE) + set(CPACK_GENERATOR "ZIP;DragNDrop") + set(CPACK_DMG_VOLUME_NAME "SimpleEngine") + set(CPACK_DMG_FORMAT "UDBZ") +else() + set(CPACK_GENERATOR "ZIP;DEB") + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name ") + set(CPACK_DEBIAN_PACKAGE_SECTION "games") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libvulkan1, libglfw3, libglm-dev, libktx-dev") +endif() + +# Include binary and resource directories in the package +install(TARGETS SimpleEngine DESTINATION bin) +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders DESTINATION share/SimpleEngine) + +# Install models and textures if they exist +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/models) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/models DESTINATION share/SimpleEngine) +endif() + +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/textures) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/textures DESTINATION share/SimpleEngine) +endif() + +# Install README if it exists +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/README.md) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/README.md DESTINATION share/SimpleEngine) +endif() diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp new file mode 100644 index 00000000..97c8c409 --- /dev/null +++ b/attachments/simple_engine/audio_system.cpp @@ -0,0 +1,453 @@ +#include "audio_system.h" + +#include +#include +#include + +#include "renderer.h" + +// Concrete implementation of AudioSource +class ConcreteAudioSource : public AudioSource { +public: + ConcreteAudioSource(const std::string& name) : name(name) {} + ~ConcreteAudioSource() override = default; + + void Play() override { + playing = true; + std::cout << "Playing audio source: " << name << std::endl; + } + + void Pause() override { + playing = false; + std::cout << "Pausing audio source: " << name << std::endl; + } + + void Stop() override { + playing = false; + std::cout << "Stopping audio source: " << name << std::endl; + } + + void SetVolume(float volume) override { + this->volume = volume; + std::cout << "Setting volume of audio source " << name << " to " << volume << std::endl; + } + + void SetLoop(bool loop) override { + this->loop = loop; + std::cout << "Setting loop of audio source " << name << " to " << (loop ? "true" : "false") << std::endl; + } + + void SetPosition(float x, float y, float z) override { + position[0] = x; + position[1] = y; + position[2] = z; + std::cout << "Setting position of audio source " << name << " to (" << x << ", " << y << ", " << z << ")" << std::endl; + } + + void SetVelocity(float x, float y, float z) override { + velocity[0] = x; + velocity[1] = y; + velocity[2] = z; + std::cout << "Setting velocity of audio source " << name << " to (" << x << ", " << y << ", " << z << ")" << std::endl; + } + + bool IsPlaying() const override { + return playing; + } + +private: + std::string name; + bool playing = false; + bool loop = false; + float volume = 1.0f; + float position[3] = {0.0f, 0.0f, 0.0f}; + float velocity[3] = {0.0f, 0.0f, 0.0f}; +}; + +AudioSystem::AudioSystem() { + // Constructor implementation +} + +AudioSystem::~AudioSystem() { + // Destructor implementation + sources.clear(); + audioData.clear(); + + // Clean up HRTF buffers + cleanupHRTFBuffers(); +} + +bool AudioSystem::Initialize(Renderer* renderer) { + // This is a placeholder implementation + // In a real implementation, this would initialize the audio API (e.g., OpenAL) + + std::cout << "Initializing audio system" << std::endl; + + // Store the renderer for compute shader support + this->renderer = renderer; + + initialized = true; + return true; +} + +void AudioSystem::Update(float deltaTime) { + // This is a placeholder implementation + // In a real implementation, this would update the audio system + + // Update listener position, orientation, and velocity based on camera + + // Update audio sources + for (auto& source : sources) { + // Update source properties + } +} + +bool AudioSystem::LoadAudio(const std::string& filename, const std::string& name) { + // This is a placeholder implementation + // In a real implementation, this would load the audio file + + std::cout << "Loading audio file: " << filename << " as " << name << std::endl; + + // Simulate loading audio data + std::vector data(1024, 0); // Dummy data + audioData[name] = data; + + return true; +} + +AudioSource* AudioSystem::CreateAudioSource(const std::string& name) { + // Check if the audio data exists + auto it = audioData.find(name); + if (it == audioData.end()) { + std::cerr << "AudioSystem::CreateAudioSource: Audio data not found: " << name << std::endl; + return nullptr; + } + + // Create a new audio source + auto source = std::make_unique(name); + + // Store the source + AudioSource* sourcePtr = source.get(); + sources.push_back(std::move(source)); + + std::cout << "Audio source created: " << name << std::endl; + return sourcePtr; +} + +void AudioSystem::SetListenerPosition(float x, float y, float z) { + listenerPosition[0] = x; + listenerPosition[1] = y; + listenerPosition[2] = z; + + std::cout << "Setting listener position to (" << x << ", " << y << ", " << z << ")" << std::endl; +} + +void AudioSystem::SetListenerOrientation(float forwardX, float forwardY, float forwardZ, + float upX, float upY, float upZ) { + listenerOrientation[0] = forwardX; + listenerOrientation[1] = forwardY; + listenerOrientation[2] = forwardZ; + listenerOrientation[3] = upX; + listenerOrientation[4] = upY; + listenerOrientation[5] = upZ; + + std::cout << "Setting listener orientation to forward=(" << forwardX << ", " << forwardY << ", " << forwardZ << "), " + << "up=(" << upX << ", " << upY << ", " << upZ << ")" << std::endl; +} + +void AudioSystem::SetListenerVelocity(float x, float y, float z) { + listenerVelocity[0] = x; + listenerVelocity[1] = y; + listenerVelocity[2] = z; + + std::cout << "Setting listener velocity to (" << x << ", " << y << ", " << z << ")" << std::endl; +} + +void AudioSystem::SetMasterVolume(float volume) { + masterVolume = volume; + + std::cout << "Setting master volume to " << volume << std::endl; +} + +void AudioSystem::EnableHRTF(bool enable) { + hrtfEnabled = enable; + std::cout << "HRTF processing " << (enable ? "enabled" : "disabled") << std::endl; +} + +bool AudioSystem::IsHRTFEnabled() const { + return hrtfEnabled; +} + +bool AudioSystem::LoadHRTFData(const std::string& filename) { + // This is a placeholder implementation + // In a real implementation, this would load HRTF data from a file + + std::cout << "Loading HRTF data from: " << filename << std::endl; + + // Simulate loading HRTF data + // In a real implementation, this would parse a file containing HRTF impulse responses + + // Create some dummy HRTF data for testing + // Typically, HRTF data consists of impulse responses for different directions + const uint32_t hrtfSampleCount = 256; // Number of samples per impulse response + const uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) + const uint32_t channelCount = 2; // Stereo (left and right ears) + + // Resize the HRTF data vector + hrtfData.resize(hrtfSampleCount * positionCount * channelCount); + + // Fill with dummy data (simple exponential decay) + for (uint32_t pos = 0; pos < positionCount; pos++) { + for (uint32_t channel = 0; channel < channelCount; channel++) { + for (uint32_t i = 0; i < hrtfSampleCount; i++) { + float value = std::exp(-static_cast(i) / 20.0f) * 0.5f; + // Add some variation based on position and channel + value *= (1.0f + 0.2f * std::sin(pos * 0.1f + channel * 3.14159f)); + + uint32_t index = pos * hrtfSampleCount * channelCount + channel * hrtfSampleCount + i; + hrtfData[index] = value; + } + } + } + + // Store HRTF parameters + hrtfSize = hrtfSampleCount; + numHrtfPositions = positionCount; + + return true; +} + +bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition) { + if (!hrtfEnabled || !renderer || !renderer->IsInitialized()) { + // If HRTF is disabled or renderer is not available, just copy input to output + for (uint32_t i = 0; i < sampleCount; i++) { + outputBuffer[i * 2] = inputBuffer[i]; // Left channel + outputBuffer[i * 2 + 1] = inputBuffer[i]; // Right channel + } + return true; + } + + // Create buffers for HRTF processing if they don't exist or if the sample count has changed + if (!createHRTFBuffers(sampleCount)) { + std::cerr << "Failed to create HRTF buffers" << std::endl; + return false; + } + + // Copy input data to input buffer + void* data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); + memcpy(data, inputBuffer, sampleCount * sizeof(float)); + inputBufferMemory.unmapMemory(); + + // Set up HRTF parameters + struct HRTFParams { + float sourcePosition[3]; + float listenerPosition[3]; + float listenerOrientation[6]; // Forward (3) and up (3) vectors + uint32_t sampleCount; + uint32_t hrtfSize; + uint32_t numHrtfPositions; + float padding; // For alignment + } params; + + // Copy source and listener positions + memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); + memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); + memcpy(params.listenerOrientation, listenerOrientation, sizeof(float) * 6); + params.sampleCount = sampleCount; + params.hrtfSize = hrtfSize; + params.numHrtfPositions = numHrtfPositions; + params.padding = 0.0f; + + // Copy parameters to parameter buffer + data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); + memcpy(data, ¶ms, sizeof(HRTFParams)); + paramsBufferMemory.unmapMemory(); + + // Dispatch compute shader + // In a real implementation, this would use a compute shader to perform HRTF convolution + // For now, we'll simulate the HRTF processing on the CPU + + // Calculate direction from listener to source + float direction[3]; + direction[0] = sourcePosition[0] - listenerPosition[0]; + direction[1] = sourcePosition[1] - listenerPosition[1]; + direction[2] = sourcePosition[2] - listenerPosition[2]; + + // Normalize direction + float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); + if (length > 0.0001f) { + direction[0] /= length; + direction[1] /= length; + direction[2] /= length; + } else { + direction[0] = 0.0f; + direction[1] = 0.0f; + direction[2] = -1.0f; // Default to front + } + + // Calculate azimuth and elevation + float azimuth = std::atan2(direction[0], direction[2]); + float elevation = std::asin(std::max(-1.0f, std::min(1.0f, direction[1]))); + + // Convert to indices + int azimuthIndex = static_cast((azimuth + M_PI) / (2.0f * M_PI) * 36.0f) % 36; + int elevationIndex = static_cast((elevation + M_PI / 2.0f) / M_PI * 13.0f); + elevationIndex = std::max(0, std::min(12, elevationIndex)); + + // Get HRTF index + int hrtfIndex = elevationIndex * 36 + azimuthIndex; + hrtfIndex = std::min(hrtfIndex, static_cast(numHrtfPositions) - 1); + + // Perform convolution for left and right ears + for (uint32_t i = 0; i < sampleCount; i++) { + float leftSample = 0.0f; + float rightSample = 0.0f; + + // Convolve with HRTF impulse response + for (uint32_t j = 0; j < hrtfSize && j <= i; j++) { + uint32_t hrtfLeftIndex = hrtfIndex * hrtfSize * 2 + j; + uint32_t hrtfRightIndex = hrtfIndex * hrtfSize * 2 + hrtfSize + j; + + if (hrtfLeftIndex < hrtfData.size() && hrtfRightIndex < hrtfData.size()) { + leftSample += inputBuffer[i - j] * hrtfData[hrtfLeftIndex]; + rightSample += inputBuffer[i - j] * hrtfData[hrtfRightIndex]; + } + } + + // Apply distance attenuation + float distanceAttenuation = 1.0f / std::max(1.0f, length); + leftSample *= distanceAttenuation; + rightSample *= distanceAttenuation; + + // Write to output buffer + outputBuffer[i * 2] = leftSample; + outputBuffer[i * 2 + 1] = rightSample; + } + + return true; +} + +bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { + // Clean up existing buffers + cleanupHRTFBuffers(); + + if (!renderer) { + std::cerr << "AudioSystem::createHRTFBuffers: Renderer is null" << std::endl; + return false; + } + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + // Check if device is valid (using operator bool() instead of ! operator) + if (*device == nullptr) { + std::cerr << "AudioSystem::createHRTFBuffers: Vulkan device is null" << std::endl; + return false; + } + + try { + // Create input buffer (mono audio) + vk::BufferCreateInfo inputBufferInfo; + inputBufferInfo.size = sampleCount * sizeof(float); + inputBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + inputBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + inputBuffer = vk::raii::Buffer(device, inputBufferInfo); + + vk::MemoryRequirements inputMemRequirements = inputBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo inputAllocInfo; + inputAllocInfo.allocationSize = inputMemRequirements.size; + inputAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + inputMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + inputBufferMemory = vk::raii::DeviceMemory(device, inputAllocInfo); + inputBuffer.bindMemory(*inputBufferMemory, 0); + + // Create output buffer (stereo audio) + vk::BufferCreateInfo outputBufferInfo; + outputBufferInfo.size = sampleCount * 2 * sizeof(float); // Stereo (2 channels) + outputBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + outputBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + outputBuffer = vk::raii::Buffer(device, outputBufferInfo); + + vk::MemoryRequirements outputMemRequirements = outputBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo outputAllocInfo; + outputAllocInfo.allocationSize = outputMemRequirements.size; + outputAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + outputMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + outputBufferMemory = vk::raii::DeviceMemory(device, outputAllocInfo); + outputBuffer.bindMemory(*outputBufferMemory, 0); + + // Create HRTF data buffer + vk::BufferCreateInfo hrtfBufferInfo; + hrtfBufferInfo.size = hrtfData.size() * sizeof(float); + hrtfBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + hrtfBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + hrtfBuffer = vk::raii::Buffer(device, hrtfBufferInfo); + + vk::MemoryRequirements hrtfMemRequirements = hrtfBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo hrtfAllocInfo; + hrtfAllocInfo.allocationSize = hrtfMemRequirements.size; + hrtfAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + hrtfMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + hrtfBufferMemory = vk::raii::DeviceMemory(device, hrtfAllocInfo); + hrtfBuffer.bindMemory(*hrtfBufferMemory, 0); + + // Copy HRTF data to buffer + void* hrtfMappedMemory = hrtfBufferMemory.mapMemory(0, hrtfData.size() * sizeof(float)); + memcpy(hrtfMappedMemory, hrtfData.data(), hrtfData.size() * sizeof(float)); + hrtfBufferMemory.unmapMemory(); + + // Create parameters buffer + vk::BufferCreateInfo paramsBufferInfo; + paramsBufferInfo.size = 256; // Size large enough for all parameters + paramsBufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; + paramsBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + paramsBuffer = vk::raii::Buffer(device, paramsBufferInfo); + + vk::MemoryRequirements paramsMemRequirements = paramsBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo paramsAllocInfo; + paramsAllocInfo.allocationSize = paramsMemRequirements.size; + paramsAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + paramsMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + paramsBufferMemory = vk::raii::DeviceMemory(device, paramsAllocInfo); + paramsBuffer.bindMemory(*paramsBufferMemory, 0); + + std::cout << "HRTF buffers created successfully" << std::endl; + return true; + } + catch (const std::exception& e) { + std::cerr << "Error creating HRTF buffers: " << e.what() << std::endl; + cleanupHRTFBuffers(); + return false; + } +} + +void AudioSystem::cleanupHRTFBuffers() { + // With RAII, we just need to set the resources to nullptr + // The destructors will handle the cleanup + inputBuffer = nullptr; + inputBufferMemory = nullptr; + outputBuffer = nullptr; + outputBufferMemory = nullptr; + hrtfBuffer = nullptr; + hrtfBufferMemory = nullptr; + paramsBuffer = nullptr; + paramsBufferMemory = nullptr; +} diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h new file mode 100644 index 00000000..6d3f38b9 --- /dev/null +++ b/attachments/simple_engine/audio_system.h @@ -0,0 +1,236 @@ +#pragma once + +#include +#include +#include +#include +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include + +/** + * @brief Class representing an audio source. + */ +class AudioSource { +public: + /** + * @brief Default constructor. + */ + AudioSource() = default; + + /** + * @brief Destructor for proper cleanup. + */ + virtual ~AudioSource() = default; + + /** + * @brief Play the audio source. + */ + virtual void Play() = 0; + + /** + * @brief Pause the audio source. + */ + virtual void Pause() = 0; + + /** + * @brief Stop the audio source. + */ + virtual void Stop() = 0; + + /** + * @brief Set the volume of the audio source. + * @param volume The volume (0.0f to 1.0f). + */ + virtual void SetVolume(float volume) = 0; + + /** + * @brief Set whether the audio source should loop. + * @param loop Whether to loop. + */ + virtual void SetLoop(bool loop) = 0; + + /** + * @brief Set the position of the audio source in 3D space. + * @param x The x-coordinate. + * @param y The y-coordinate. + * @param z The z-coordinate. + */ + virtual void SetPosition(float x, float y, float z) = 0; + + /** + * @brief Set the velocity of the audio source in 3D space. + * @param x The x-component. + * @param y The y-component. + * @param z The z-component. + */ + virtual void SetVelocity(float x, float y, float z) = 0; + + /** + * @brief Check if the audio source is playing. + * @return True if playing, false otherwise. + */ + virtual bool IsPlaying() const = 0; +}; + +// Forward declarations +class Renderer; + +/** + * @brief Class for managing audio. + */ +class AudioSystem { +public: + /** + * @brief Default constructor. + */ + AudioSystem(); + + /** + * @brief Destructor for proper cleanup. + */ + ~AudioSystem(); + + /** + * @brief Initialize the audio system. + * @param renderer Pointer to the renderer for compute shader support. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(Renderer* renderer = nullptr); + + /** + * @brief Update the audio system. + * @param deltaTime The time elapsed since the last update. + */ + void Update(float deltaTime); + + /** + * @brief Load an audio file. + * @param filename The path to the audio file. + * @param name The name to assign to the audio. + * @return True if loading was successful, false otherwise. + */ + bool LoadAudio(const std::string& filename, const std::string& name); + + /** + * @brief Create an audio source. + * @param name The name of the audio to use. + * @return Pointer to the created audio source, or nullptr if creation failed. + */ + AudioSource* CreateAudioSource(const std::string& name); + + /** + * @brief Set the listener position in 3D space. + * @param x The x-coordinate. + * @param y The y-coordinate. + * @param z The z-coordinate. + */ + void SetListenerPosition(float x, float y, float z); + + /** + * @brief Set the listener orientation in 3D space. + * @param forwardX The x-component of the forward vector. + * @param forwardY The y-component of the forward vector. + * @param forwardZ The z-component of the forward vector. + * @param upX The x-component of the up vector. + * @param upY The y-component of the up vector. + * @param upZ The z-component of the up vector. + */ + void SetListenerOrientation(float forwardX, float forwardY, float forwardZ, + float upX, float upY, float upZ); + + /** + * @brief Set the listener velocity in 3D space. + * @param x The x-component. + * @param y The y-component. + * @param z The z-component. + */ + void SetListenerVelocity(float x, float y, float z); + + /** + * @brief Set the master volume. + * @param volume The volume (0.0f to 1.0f). + */ + void SetMasterVolume(float volume); + + /** + * @brief Enable HRTF (Head-Related Transfer Function) processing. + * @param enable Whether to enable HRTF processing. + */ + void EnableHRTF(bool enable); + + /** + * @brief Check if HRTF processing is enabled. + * @return True if HRTF processing is enabled, false otherwise. + */ + bool IsHRTFEnabled() const; + + /** + * @brief Load HRTF data from a file. + * @param filename The path to the HRTF data file. + * @return True if loading was successful, false otherwise. + */ + bool LoadHRTFData(const std::string& filename); + + /** + * @brief Process audio data with HRTF. + * @param inputBuffer The input audio buffer. + * @param outputBuffer The output audio buffer. + * @param sampleCount The number of samples to process. + * @param sourcePosition The position of the sound source. + * @return True if processing was successful, false otherwise. + */ + bool ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition); + +private: + // Loaded audio data + std::unordered_map> audioData; + + // Audio sources + std::vector> sources; + + // Listener properties + float listenerPosition[3] = {0.0f, 0.0f, 0.0f}; + float listenerOrientation[6] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; + float listenerVelocity[3] = {0.0f, 0.0f, 0.0f}; + + // Master volume + float masterVolume = 1.0f; + + // Whether the audio system is initialized + bool initialized = false; + + // HRTF processing + bool hrtfEnabled = false; + std::vector hrtfData; + uint32_t hrtfSize = 0; + uint32_t numHrtfPositions = 0; + + // Renderer for compute shader support + Renderer* renderer = nullptr; + + // Vulkan resources for HRTF processing + vk::raii::Buffer inputBuffer = nullptr; + vk::raii::DeviceMemory inputBufferMemory = nullptr; + vk::raii::Buffer outputBuffer = nullptr; + vk::raii::DeviceMemory outputBufferMemory = nullptr; + vk::raii::Buffer hrtfBuffer = nullptr; + vk::raii::DeviceMemory hrtfBufferMemory = nullptr; + vk::raii::Buffer paramsBuffer = nullptr; + vk::raii::DeviceMemory paramsBufferMemory = nullptr; + + /** + * @brief Create buffers for HRTF processing. + * @param sampleCount The number of samples to process. + * @return True if creation was successful, false otherwise. + */ + bool createHRTFBuffers(uint32_t sampleCount); + + /** + * @brief Clean up HRTF buffers. + */ + void cleanupHRTFBuffers(); +}; diff --git a/attachments/simple_engine/camera_component.cpp b/attachments/simple_engine/camera_component.cpp new file mode 100644 index 00000000..012fbbd4 --- /dev/null +++ b/attachments/simple_engine/camera_component.cpp @@ -0,0 +1,63 @@ +#include "camera_component.h" + +#include "entity.h" + +// Most of the CameraComponent class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// This implementation corresponds to the Camera_Transformations chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc + +// Initializes the camera by updating the view and projection matrices +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#camera-initialization +void CameraComponent::Initialize() { + UpdateViewMatrix(); + UpdateProjectionMatrix(); +} + +// Returns the view matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#accessing-camera-matrices +const glm::mat4& CameraComponent::GetViewMatrix() { + if (viewMatrixDirty) { + UpdateViewMatrix(); + } + return viewMatrix; +} + +// Returns the projection matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#accessing-camera-matrices +const glm::mat4& CameraComponent::GetProjectionMatrix() { + if (projectionMatrixDirty) { + UpdateProjectionMatrix(); + } + return projectionMatrix; +} + +// Updates the view matrix based on the camera's position and orientation +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#view-matrix +void CameraComponent::UpdateViewMatrix() { + auto transformComponent = owner->GetComponent(); + if (transformComponent) { + glm::vec3 position = transformComponent->GetPosition(); + viewMatrix = glm::lookAt(position, target, up); + } else { + viewMatrix = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), target, up); + } + viewMatrixDirty = false; +} + +// Updates the projection matrix based on the camera's projection type and parameters +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#projection-matrix +void CameraComponent::UpdateProjectionMatrix() { + if (projectionType == ProjectionType::Perspective) { + projectionMatrix = glm::perspective(glm::radians(fieldOfView), aspectRatio, nearPlane, farPlane); + } else { + float halfWidth = orthoWidth * 0.5f; + float halfHeight = orthoHeight * 0.5f; + projectionMatrix = glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, nearPlane, farPlane); + } + // Flip Y-axis for Vulkan coordinate system + // @see en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc#coordinate-system-differences + projectionMatrix[1][1] *= -1; + projectionMatrixDirty = false; +} diff --git a/attachments/simple_engine/camera_component.h b/attachments/simple_engine/camera_component.h new file mode 100644 index 00000000..9fd05834 --- /dev/null +++ b/attachments/simple_engine/camera_component.h @@ -0,0 +1,184 @@ +#pragma once + +#include +#include + +#include "component.h" +#include "transform_component.h" +#include "entity.h" + +/** + * @brief Component that handles the camera view and projection. + * + * This class implements the camera system as described in the Camera_Transformations chapter: + * @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc + */ +class CameraComponent : public Component { +public: + enum class ProjectionType { + Perspective, + Orthographic + }; + +private: + ProjectionType projectionType = ProjectionType::Perspective; + + // Perspective projection parameters + float fieldOfView = 45.0f; + float aspectRatio = 16.0f / 9.0f; + + // Orthographic projection parameters + float orthoWidth = 10.0f; + float orthoHeight = 10.0f; + + // Common parameters + float nearPlane = 0.1f; + float farPlane = 100.0f; + + // Matrices + glm::mat4 viewMatrix = glm::mat4(1.0f); + glm::mat4 projectionMatrix = glm::mat4(1.0f); + + // Camera properties + glm::vec3 target = {0.0f, 0.0f, 0.0f}; + glm::vec3 up = {0.0f, 1.0f, 0.0f}; + + bool viewMatrixDirty = true; + bool projectionMatrixDirty = true; + +public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit CameraComponent(const std::string& componentName = "CameraComponent") + : Component(componentName) {} + + /** + * @brief Initialize the camera component. + */ + void Initialize() override; + + /** + * @brief Set the projection type. + * @param type The projection type. + */ + void SetProjectionType(ProjectionType type) { + projectionType = type; + projectionMatrixDirty = true; + } + + /** + * @brief Get the projection type. + * @return The projection type. + */ + ProjectionType GetProjectionType() const { + return projectionType; + } + + /** + * @brief Set the field of view for perspective projection. + * @param fov The field of view in degrees. + */ + void SetFieldOfView(float fov) { + fieldOfView = fov; + projectionMatrixDirty = true; + } + + /** + * @brief Get the field of view. + * @return The field of view in degrees. + */ + float GetFieldOfView() const { + return fieldOfView; + } + + /** + * @brief Set the aspect ratio for perspective projection. + * @param ratio The aspect ratio (width / height). + */ + void SetAspectRatio(float ratio) { + aspectRatio = ratio; + projectionMatrixDirty = true; + } + + /** + * @brief Get the aspect ratio. + * @return The aspect ratio. + */ + float GetAspectRatio() const { + return aspectRatio; + } + + /** + * @brief Set the orthographic width and height. + * @param width The width of the orthographic view. + * @param height The height of the orthographic view. + */ + void SetOrthographicSize(float width, float height) { + orthoWidth = width; + orthoHeight = height; + projectionMatrixDirty = true; + } + + /** + * @brief Set the near and far planes. + * @param near The near plane distance. + * @param far The far plane distance. + */ + void SetClipPlanes(float near, float far) { + nearPlane = near; + farPlane = far; + projectionMatrixDirty = true; + } + + /** + * @brief Set the camera target. + * @param newTarget The new target position. + */ + void SetTarget(const glm::vec3& newTarget) { + target = newTarget; + viewMatrixDirty = true; + } + + /** + * @brief Set the camera up vector. + * @param newUp The new up vector. + */ + void SetUp(const glm::vec3& newUp) { + up = newUp; + viewMatrixDirty = true; + } + + /** + * @brief Get the view matrix. + * @return The view matrix. + */ + const glm::mat4& GetViewMatrix(); + + /** + * @brief Get the projection matrix. + * @return The projection matrix. + */ + const glm::mat4& GetProjectionMatrix(); + + /** + * @brief Get the camera position. + * @return The camera position. + */ + glm::vec3 GetPosition() const { + auto transform = GetOwner()->GetComponent(); + return transform ? transform->GetPosition() : glm::vec3(0.0f, 0.0f, 0.0f); + } + +private: + /** + * @brief Update the view matrix based on the camera position and target. + */ + void UpdateViewMatrix(); + + /** + * @brief Update the projection matrix based on the projection type and parameters. + */ + void UpdateProjectionMatrix(); +}; diff --git a/attachments/simple_engine/component.cpp b/attachments/simple_engine/component.cpp new file mode 100644 index 00000000..157ade0b --- /dev/null +++ b/attachments/simple_engine/component.cpp @@ -0,0 +1,11 @@ +#include "component.h" +#include "entity.h" + +// Most of the Component class implementation is in the header file +// This file is mainly for any methods that need to access the Entity class +// to avoid circular dependencies +// +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// https://github.com/KhronosGroup/Vulkan-Tutorial/blob/master/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc + +// No additional implementation needed for now diff --git a/attachments/simple_engine/component.h b/attachments/simple_engine/component.h new file mode 100644 index 00000000..85327d32 --- /dev/null +++ b/attachments/simple_engine/component.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +// Forward declaration +class Entity; + +/** + * @brief Base class for all components in the engine. + * + * Components are the building blocks of the entity-component system. + * Each component encapsulates a specific behavior or property. + * + * This class implements the component system as described in the Engine_Architecture chapter: + * https://github.com/KhronosGroup/Vulkan-Tutorial/blob/master/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc + */ +class Component { +protected: + Entity* owner = nullptr; + std::string name; + bool active = true; + +public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit Component(const std::string& componentName = "Component") : name(componentName) {} + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Component() = default; + + /** + * @brief Initialize the component. + * Called when the component is added to an entity. + */ + virtual void Initialize() {} + + /** + * @brief Update the component. + * Called every frame. + * @param deltaTime The time elapsed since the last frame. + */ + virtual void Update(float deltaTime) {} + + /** + * @brief Render the component. + * Called during the rendering phase. + */ + virtual void Render() {} + + /** + * @brief Set the owner entity of this component. + * @param entity The entity that owns this component. + */ + void SetOwner(Entity* entity) { owner = entity; } + + /** + * @brief Get the owner entity of this component. + * @return The entity that owns this component. + */ + Entity* GetOwner() const { return owner; } + + /** + * @brief Get the name of the component. + * @return The name of the component. + */ + const std::string& GetName() const { return name; } + + /** + * @brief Check if the component is active. + * @return True if the component is active, false otherwise. + */ + bool IsActive() const { return active; } + + /** + * @brief Set the active state of the component. + * @param isActive The new active state. + */ + void SetActive(bool isActive) { active = isActive; } +}; diff --git a/attachments/simple_engine/crash_reporter.h b/attachments/simple_engine/crash_reporter.h new file mode 100644 index 00000000..82f1b356 --- /dev/null +++ b/attachments/simple_engine/crash_reporter.h @@ -0,0 +1,291 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "dbghelp.lib") +#elif defined(__APPLE__) || defined(__linux__) +#include +#include +#include +#endif + +#include "debug_system.h" + +/** + * @brief Class for crash reporting and minidump generation. + * + * This class implements the crash reporting system as described in the Tooling chapter: + * @see en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc + */ +class CrashReporter { +public: + /** + * @brief Get the singleton instance of the crash reporter. + * @return Reference to the crash reporter instance. + */ + static CrashReporter& GetInstance() { + static CrashReporter instance; + return instance; + } + + /** + * @brief Initialize the crash reporter. + * @param minidumpDir The directory to store minidumps. + * @param appName The name of the application. + * @param appVersion The version of the application. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& minidumpDir = "crashes", + const std::string& appName = "SimpleEngine", + const std::string& appVersion = "1.0.0") { + std::lock_guard lock(mutex); + + this->minidumpDir = minidumpDir; + this->appName = appName; + this->appVersion = appVersion; + + // Create minidump directory if it doesn't exist + #ifdef _WIN32 + CreateDirectoryA(minidumpDir.c_str(), NULL); + #else + std::string command = "mkdir -p " + minidumpDir; + system(command.c_str()); + #endif + + // Install crash handlers + InstallCrashHandlers(); + + // Register with debug system + DebugSystem::GetInstance().SetCrashHandler([this](const std::string& message) { + this->HandleCrash(message); + }); + + LOG_INFO("CrashReporter", "Crash reporter initialized"); + initialized = true; + return true; + } + + /** + * @brief Clean up crash reporter resources. + */ + void Cleanup() { + std::lock_guard lock(mutex); + + if (initialized) { + // Uninstall crash handlers + UninstallCrashHandlers(); + + LOG_INFO("CrashReporter", "Crash reporter shutting down"); + initialized = false; + } + } + + /** + * @brief Handle a crash. + * @param message The crash message. + */ + void HandleCrash(const std::string& message) { + std::lock_guard lock(mutex); + + LOG_FATAL("CrashReporter", "Crash detected: " + message); + + // Generate minidump + GenerateMinidump(message); + + // Call registered callbacks + for (const auto& callback : crashCallbacks) { + callback(message); + } + } + + /** + * @brief Register a crash callback. + * @param callback The callback function to be called when a crash occurs. + * @return An ID that can be used to unregister the callback. + */ + int RegisterCrashCallback(std::function callback) { + std::lock_guard lock(mutex); + + int id = nextCallbackId++; + crashCallbacks[id] = callback; + return id; + } + + /** + * @brief Unregister a crash callback. + * @param id The ID of the callback to unregister. + */ + void UnregisterCrashCallback(int id) { + std::lock_guard lock(mutex); + + crashCallbacks.erase(id); + } + + /** + * @brief Generate a minidump. + * @param message The crash message. + */ + void GenerateMinidump(const std::string& message) { + // Get current time for filename + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + char timeStr[20]; + std::strftime(timeStr, sizeof(timeStr), "%Y%m%d_%H%M%S", std::localtime(&time)); + + // Create minidump filename + std::string filename = minidumpDir + "/" + appName + "_" + timeStr + ".dmp"; + + LOG_INFO("CrashReporter", "Generating minidump: " + filename); + + // Generate minidump based on platform + #ifdef _WIN32 + // Windows implementation + HANDLE hFile = CreateFileA( + filename.c_str(), + GENERIC_WRITE, + 0, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + + if (hFile != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION exInfo; + exInfo.ThreadId = GetCurrentThreadId(); + exInfo.ExceptionPointers = NULL; // Would be set in a real exception handler + exInfo.ClientPointers = FALSE; + + MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + hFile, + MiniDumpNormal, + &exInfo, + NULL, + NULL + ); + + CloseHandle(hFile); + } + #else + // Unix implementation + std::ofstream file(filename, std::ios::out | std::ios::binary); + if (file.is_open()) { + // Get backtrace + void* callstack[128]; + int frames = backtrace(callstack, 128); + char** symbols = backtrace_symbols(callstack, frames); + + // Write header + file << "Crash Report for " << appName << " " << appVersion << std::endl; + file << "Timestamp: " << timeStr << std::endl; + file << "Message: " << message << std::endl; + file << std::endl; + + // Write backtrace + file << "Backtrace:" << std::endl; + for (int i = 0; i < frames; i++) { + file << symbols[i] << std::endl; + } + + free(symbols); + file.close(); + } + #endif + + LOG_INFO("CrashReporter", "Minidump generated: " + filename); + } + +private: + // Private constructor for singleton + CrashReporter() = default; + + // Delete copy constructor and assignment operator + CrashReporter(const CrashReporter&) = delete; + CrashReporter& operator=(const CrashReporter&) = delete; + + // Mutex for thread safety + std::mutex mutex; + + // Initialization flag + bool initialized = false; + + // Minidump directory + std::string minidumpDir = "crashes"; + + // Application info + std::string appName = "SimpleEngine"; + std::string appVersion = "1.0.0"; + + // Crash callbacks + std::unordered_map> crashCallbacks; + int nextCallbackId = 0; + + /** + * @brief Install platform-specific crash handlers. + */ + void InstallCrashHandlers() { + #ifdef _WIN32 + // Windows implementation + SetUnhandledExceptionFilter([](EXCEPTION_POINTERS* exInfo) -> LONG { + CrashReporter::GetInstance().HandleCrash("Unhandled exception"); + return EXCEPTION_EXECUTE_HANDLER; + }); + #else + // Unix implementation + signal(SIGSEGV, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Segmentation fault"); + exit(1); + }); + + signal(SIGABRT, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Abort"); + exit(1); + }); + + signal(SIGFPE, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Floating point exception"); + exit(1); + }); + + signal(SIGILL, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Illegal instruction"); + exit(1); + }); + #endif + } + + /** + * @brief Uninstall platform-specific crash handlers. + */ + void UninstallCrashHandlers() { + #ifdef _WIN32 + // Windows implementation + SetUnhandledExceptionFilter(NULL); + #else + // Unix implementation + signal(SIGSEGV, SIG_DFL); + signal(SIGABRT, SIG_DFL); + signal(SIGFPE, SIG_DFL); + signal(SIGILL, SIG_DFL); + #endif + } +}; + +// Convenience macro for simulating a crash (for testing) +#define SIMULATE_CRASH(message) CrashReporter::GetInstance().HandleCrash(message) diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h new file mode 100644 index 00000000..3f3a0e55 --- /dev/null +++ b/attachments/simple_engine/debug_system.h @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Enum for different log levels. + */ +enum class LogLevel { + Debug, + Info, + Warning, + Error, + Fatal +}; + +/** + * @brief Class for managing debugging and logging. + * + * This class implements the debugging system as described in the Tooling chapter: + * @see en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc + */ +class DebugSystem { +public: + /** + * @brief Get the singleton instance of the debug system. + * @return Reference to the debug system instance. + */ + static DebugSystem& GetInstance() { + static DebugSystem instance; + return instance; + } + + /** + * @brief Initialize the debug system. + * @param logFilePath The path to the log file. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& logFilePath = "engine.log") { + std::lock_guard lock(mutex); + + // Open log file + logFile.open(logFilePath, std::ios::out | std::ios::trunc); + if (!logFile.is_open()) { + std::cerr << "Failed to open log file: " << logFilePath << std::endl; + return false; + } + + // Log initialization + Log(LogLevel::Info, "DebugSystem", "Debug system initialized"); + + initialized = true; + return true; + } + + /** + * @brief Clean up debug system resources. + */ + void Cleanup() { + std::lock_guard lock(mutex); + + if (initialized) { + // Log cleanup + Log(LogLevel::Info, "DebugSystem", "Debug system shutting down"); + + // Close log file + if (logFile.is_open()) { + logFile.close(); + } + + initialized = false; + } + } + + /** + * @brief Log a message. + * @param level The log level. + * @param tag The tag for the log message. + * @param message The log message. + */ + void Log(LogLevel level, const std::string& tag, const std::string& message) { + std::lock_guard lock(mutex); + + // Get current time + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + + char timeStr[20]; + std::strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", std::localtime(&time)); + + // Format log message + std::string levelStr; + switch (level) { + case LogLevel::Debug: + levelStr = "DEBUG"; + break; + case LogLevel::Info: + levelStr = "INFO"; + break; + case LogLevel::Warning: + levelStr = "WARNING"; + break; + case LogLevel::Error: + levelStr = "ERROR"; + break; + case LogLevel::Fatal: + levelStr = "FATAL"; + break; + } + + std::string formattedMessage = + std::string(timeStr) + "." + std::to_string(ms.count()) + + " [" + levelStr + "] " + + "[" + tag + "] " + + message; + + // Write to console + if (level >= LogLevel::Warning) { + std::cerr << formattedMessage << std::endl; + } else { + std::cout << formattedMessage << std::endl; + } + + // Write to log file + if (logFile.is_open()) { + logFile << formattedMessage << std::endl; + logFile.flush(); + } + + // Call registered callbacks + for (const auto& callback : logCallbacks) { + callback(level, tag, message); + } + + // If fatal, trigger crash handler + if (level == LogLevel::Fatal && crashHandler) { + crashHandler(formattedMessage); + } + } + + /** + * @brief Register a log callback. + * @param callback The callback function to be called when a log message is generated. + * @return An ID that can be used to unregister the callback. + */ + int RegisterLogCallback(std::function callback) { + std::lock_guard lock(mutex); + + int id = nextCallbackId++; + logCallbacks[id] = callback; + return id; + } + + /** + * @brief Unregister a log callback. + * @param id The ID of the callback to unregister. + */ + void UnregisterLogCallback(int id) { + std::lock_guard lock(mutex); + + logCallbacks.erase(id); + } + + /** + * @brief Set the crash handler. + * @param handler The crash handler function. + */ + void SetCrashHandler(std::function handler) { + std::lock_guard lock(mutex); + + crashHandler = handler; + } + + /** + * @brief Start a performance measurement. + * @param name The name of the measurement. + */ + void StartMeasurement(const std::string& name) { + std::lock_guard lock(mutex); + + auto now = std::chrono::high_resolution_clock::now(); + measurements[name] = now; + } + + /** + * @brief End a performance measurement and log the result. + * @param name The name of the measurement. + */ + void EndMeasurement(const std::string& name) { + std::lock_guard lock(mutex); + + auto now = std::chrono::high_resolution_clock::now(); + auto it = measurements.find(name); + + if (it != measurements.end()) { + auto duration = std::chrono::duration_cast(now - it->second).count(); + Log(LogLevel::Debug, "Performance", name + ": " + std::to_string(duration) + " us"); + measurements.erase(it); + } else { + Log(LogLevel::Warning, "Performance", "No measurement started with name: " + name); + } + } + + /** + * @brief Enable or disable RenderDoc integration. + * @param enable Whether to enable RenderDoc integration. + */ + void EnableRenderDoc(bool enable) { + std::lock_guard lock(mutex); + + renderDocEnabled = enable; + Log(LogLevel::Info, "DebugSystem", std::string("RenderDoc integration ") + (enable ? "enabled" : "disabled")); + + // In a real implementation, this would initialize RenderDoc API + } + + /** + * @brief Check if RenderDoc integration is enabled. + * @return True if RenderDoc integration is enabled, false otherwise. + */ + bool IsRenderDocEnabled() const { + return renderDocEnabled; + } + + /** + * @brief Trigger a RenderDoc frame capture. + */ + void CaptureRenderDocFrame() { + std::lock_guard lock(mutex); + + if (renderDocEnabled) { + Log(LogLevel::Info, "DebugSystem", "Capturing RenderDoc frame"); + + // In a real implementation, this would trigger a RenderDoc frame capture + } else { + Log(LogLevel::Warning, "DebugSystem", "RenderDoc integration is not enabled"); + } + } + +private: + // Private constructor for singleton + DebugSystem() = default; + + // Delete copy constructor and assignment operator + DebugSystem(const DebugSystem&) = delete; + DebugSystem& operator=(const DebugSystem&) = delete; + + // Mutex for thread safety + std::mutex mutex; + + // Log file + std::ofstream logFile; + + // Initialization flag + bool initialized = false; + + // Log callbacks + std::unordered_map> logCallbacks; + int nextCallbackId = 0; + + // Crash handler + std::function crashHandler; + + // Performance measurements + std::unordered_map measurements; + + // RenderDoc integration + bool renderDocEnabled = false; +}; + +// Convenience macros for logging +#define LOG_DEBUG(tag, message) DebugSystem::GetInstance().Log(LogLevel::Debug, tag, message) +#define LOG_INFO(tag, message) DebugSystem::GetInstance().Log(LogLevel::Info, tag, message) +#define LOG_WARNING(tag, message) DebugSystem::GetInstance().Log(LogLevel::Warning, tag, message) +#define LOG_ERROR(tag, message) DebugSystem::GetInstance().Log(LogLevel::Error, tag, message) +#define LOG_FATAL(tag, message) DebugSystem::GetInstance().Log(LogLevel::Fatal, tag, message) + +// Convenience macros for performance measurement +#define MEASURE_START(name) DebugSystem::GetInstance().StartMeasurement(name) +#define MEASURE_END(name) DebugSystem::GetInstance().EndMeasurement(name) diff --git a/attachments/simple_engine/descriptor_manager.cpp b/attachments/simple_engine/descriptor_manager.cpp new file mode 100644 index 00000000..a8f6c6d6 --- /dev/null +++ b/attachments/simple_engine/descriptor_manager.cpp @@ -0,0 +1,224 @@ +#include "descriptor_manager.h" +#include +#include +#include +#include "transform_component.h" +#include "camera_component.h" + +// Constructor +DescriptorManager::DescriptorManager(VulkanDevice& device) + : device(device) { +} + +// Destructor +DescriptorManager::~DescriptorManager() { + // RAII will handle destruction +} + +// Create descriptor pool +bool DescriptorManager::createDescriptorPool(uint32_t maxSets) { + try { + // Create descriptor pool sizes + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = maxSets + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = maxSets + } + }; + + // Create descriptor pool + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = maxSets, + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data() + }; + + descriptorPool = vk::raii::DescriptorPool(device.getDevice(), poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } +} + +// Create uniform buffers for an entity +bool DescriptorManager::createUniformBuffers(Entity* entity, uint32_t maxFramesInFlight) { + try { + // Create uniform buffers for each frame in flight + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + + // Create entity resources if they don't exist + if (entityResources.find(entity) == entityResources.end()) { + entityResources[entity] = EntityResources(); + } + + // Clear existing uniform buffers + entityResources[entity].uniformBuffers.clear(); + entityResources[entity].uniformBuffersMemory.clear(); + entityResources[entity].uniformBuffersMapped.clear(); + + // Create uniform buffers + for (size_t i = 0; i < maxFramesInFlight; i++) { + // Create buffer + auto [buffer, bufferMemory] = createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eUniformBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Map memory + void* data = bufferMemory.mapMemory(0, bufferSize); + + // Store resources + entityResources[entity].uniformBuffers.push_back(std::move(buffer)); + entityResources[entity].uniformBuffersMemory.push_back(std::move(bufferMemory)); + entityResources[entity].uniformBuffersMapped.push_back(data); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create uniform buffers: " << e.what() << std::endl; + return false; + } +} + +// Create descriptor sets for an entity +bool DescriptorManager::createDescriptorSets(Entity* entity, const std::string& texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight) { + try { + // Create descriptor sets for each frame in flight + std::vector layouts(maxFramesInFlight, descriptorSetLayout); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(maxFramesInFlight), + .pSetLayouts = layouts.data() + }; + + // Allocate descriptor sets + auto descriptorSets = device.getDevice().allocateDescriptorSets(allocInfo); + + // Store descriptor sets + // Convert from vk::raii::DescriptorSet to vk::DescriptorSet + std::vector nonRaiiDescriptorSets; + for (const auto& ds : descriptorSets) { + nonRaiiDescriptorSets.push_back(*ds); + } + entityResources[entity].descriptorSets = nonRaiiDescriptorSets; + + // Update descriptor sets + for (size_t i = 0; i < maxFramesInFlight; i++) { + // Create descriptor buffer info + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *entityResources[entity].uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + // Create descriptor image info + vk::DescriptorImageInfo imageInfo{ + // These would be set based on the texture resources + // .sampler = textureSampler, + // .imageView = textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + // Create descriptor writes + std::array descriptorWrites = { + vk::WriteDescriptorSet{ + .dstSet = descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pImageInfo = nullptr, + .pBufferInfo = &bufferInfo, + .pTexelBufferView = nullptr + }, + vk::WriteDescriptorSet{ + .dstSet = descriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo, + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr + } + }; + + // Update descriptor sets + device.getDevice().updateDescriptorSets(descriptorWrites, nullptr); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor sets: " << e.what() << std::endl; + return false; + } +} + +// Update uniform buffer for an entity +void DescriptorManager::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera) { + // Update uniform buffer with the latest data + UniformBufferObject ubo{}; + + // Get entity transform + auto transform = entity->GetComponent(); + if (transform) { + ubo.model = transform->GetModelMatrix(); + } else { + ubo.model = glm::mat4(1.0f); + } + + // Get camera view and projection + if (camera) { + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + ubo.viewPos = glm::vec4(camera->GetPosition(), 1.0f); + } else { + ubo.view = glm::mat4(1.0f); + ubo.proj = glm::mat4(1.0f); + ubo.viewPos = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); + } + + // Set light position and color + ubo.lightPos = glm::vec4(0.0f, 5.0f, 0.0f, 1.0f); + ubo.lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); + + // Copy data to uniform buffer + memcpy(entityResources[entity].uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +} + +// Create buffer +std::pair DescriptorManager::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties) { + // Create buffer + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer buffer(device.getDevice(), bufferInfo); + + // Allocate memory + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = device.findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory bufferMemory(device.getDevice(), allocInfo); + + // Bind memory + buffer.bindMemory(*bufferMemory, 0); + + return {std::move(buffer), std::move(bufferMemory)}; +} diff --git a/attachments/simple_engine/descriptor_manager.h b/attachments/simple_engine/descriptor_manager.h new file mode 100644 index 00000000..c66fc5cb --- /dev/null +++ b/attachments/simple_engine/descriptor_manager.h @@ -0,0 +1,117 @@ +#pragma once + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include +#include +#define GLM_FORCE_RADIANS +#include +#include + +#include "vulkan_device.h" +#include "entity.h" + +/** + * @brief Structure for uniform buffer object. + */ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; + alignas(16) glm::vec4 lightPos; + alignas(16) glm::vec4 lightColor; + alignas(16) glm::vec4 viewPos; +}; + +class CameraComponent; + +/** + * @brief Class for managing Vulkan descriptor sets and layouts. + */ +class DescriptorManager { +public: + // Entity resources + struct EntityResources { + std::vector uniformBuffers; + std::vector uniformBuffersMemory; + std::vector uniformBuffersMapped; + std::vector descriptorSets; + }; + + /** + * @brief Constructor. + * @param device The Vulkan device. + */ + DescriptorManager(VulkanDevice& device); + + /** + * @brief Destructor. + */ + ~DescriptorManager(); + + /** + * @brief Create the descriptor pool. + * @param maxSets The maximum number of descriptor sets. + * @return True if the descriptor pool was created successfully, false otherwise. + */ + bool createDescriptorPool(uint32_t maxSets); + + /** + * @brief Create uniform buffers for an entity. + * @param entity The entity. + * @param maxFramesInFlight The maximum number of frames in flight. + * @return True if the uniform buffers were created successfully, false otherwise. + */ + bool createUniformBuffers(Entity* entity, uint32_t maxFramesInFlight); + + /** + * @brief Create descriptor sets for an entity. + * @param entity The entity. + * @param texturePath The texture path. + * @param descriptorSetLayout The descriptor set layout. + * @param maxFramesInFlight The maximum number of frames in flight. + * @return True if the descriptor sets were created successfully, false otherwise. + */ + bool createDescriptorSets(Entity* entity, const std::string& texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight); + + /** + * @brief Update uniform buffer for an entity. + * @param currentImage The current image index. + * @param entity The entity. + * @param camera The camera. + */ + void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera); + + /** + * @brief Get the descriptor pool. + * @return The descriptor pool. + */ + vk::raii::DescriptorPool& getDescriptorPool() { return descriptorPool; } + + /** + * @brief Get the entity resources. + * @return The entity resources. + */ + std::unordered_map& getEntityResources() { return entityResources; } + + /** + * @brief Get the resources for an entity. + * @param entity The entity. + * @return The entity resources. + */ + EntityResources& getEntityResources(Entity* entity) { return entityResources[entity]; } + +private: + // Vulkan device + VulkanDevice& device; + + // Descriptor pool + vk::raii::DescriptorPool descriptorPool = nullptr; + std::unordered_map entityResources; + + // Helper functions + std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); +}; diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp new file mode 100644 index 00000000..e70a7d59 --- /dev/null +++ b/attachments/simple_engine/engine.cpp @@ -0,0 +1,393 @@ +#include "engine.h" + +#include +#include +#include + +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + +Engine::Engine() + : resourceManager(std::make_unique()), + modelLoader(std::make_unique()), + audioSystem(std::make_unique()), + physicsSystem(std::make_unique()), + imguiSystem(std::make_unique()) { +} + +Engine::~Engine() { + Cleanup(); +} + +bool Engine::Initialize(const std::string& appName, int width, int height, bool enableValidationLayers) { + // Create platform +#if PLATFORM_ANDROID + // For Android, the platform is created with the android_app + // This will be handled in the android_main function + return false; +#else + platform = CreatePlatform(); + if (!platform->Initialize(appName, width, height)) { + return false; + } + + // Set resize callback + platform->SetResizeCallback([this](int width, int height) { + HandleResize(width, height); + }); + + // Set mouse callback + platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { + if (imguiSystem) { + imguiSystem->HandleMouse(x, y, buttons); + } + }); + + // Set keyboard callback + platform->SetKeyboardCallback([this](uint32_t key, bool pressed) { + if (imguiSystem) { + imguiSystem->HandleKeyboard(key, pressed); + } + }); + + // Set char callback + platform->SetCharCallback([this](uint32_t c) { + if (imguiSystem) { + imguiSystem->HandleChar(c); + } + }); + + // Create renderer + renderer = std::make_unique(platform.get()); + if (!renderer->Initialize(appName, enableValidationLayers)) { + return false; + } + + // Initialize model loader + if (!modelLoader->Initialize(renderer.get())) { + return false; + } + + // Initialize audio system + if (!audioSystem->Initialize()) { + return false; + } + + // Initialize physics system + if (!physicsSystem->Initialize()) { + return false; + } + + // Initialize ImGui system + if (!imguiSystem->Initialize(renderer.get(), width, height)) { + return false; + } + + initialized = true; + return true; +#endif +} + +void Engine::Run() { + if (!initialized) { + throw std::runtime_error("Engine not initialized"); + } + + running = true; + + // Main loop + while (running) { + // Process platform events + if (!platform->ProcessEvents()) { + running = false; + break; + } + + // Calculate delta time + deltaTime = CalculateDeltaTime(); + + // Update + Update(deltaTime); + + // Render + Render(); + } +} + +void Engine::Cleanup() { + if (initialized) { + // Wait for the device to be idle before cleaning up + if (renderer) { + renderer->WaitIdle(); + } + + // Clear entities + entities.clear(); + entityMap.clear(); + + // Clean up subsystems in reverse order of creation + imguiSystem.reset(); + physicsSystem.reset(); + audioSystem.reset(); + modelLoader.reset(); + renderer.reset(); + platform.reset(); + + initialized = false; + } +} + +Entity* Engine::CreateEntity(const std::string& name) { + // Check if an entity with this name already exists + if (entityMap.find(name) != entityMap.end()) { + return nullptr; + } + + // Create the entity + auto entity = std::make_unique(name); + Entity* entityPtr = entity.get(); + + // Add to the map and vector + entityMap[name] = entityPtr; + entities.push_back(std::move(entity)); + + return entityPtr; +} + +Entity* Engine::GetEntity(const std::string& name) { + auto it = entityMap.find(name); + if (it != entityMap.end()) { + return it->second; + } + return nullptr; +} + +bool Engine::RemoveEntity(Entity* entity) { + if (!entity) { + return false; + } + + // Find the entity in the vector + auto it = std::find_if(entities.begin(), entities.end(), + [entity](const std::unique_ptr& e) { + return e.get() == entity; + }); + + if (it != entities.end()) { + // Remove from the map + entityMap.erase(entity->GetName()); + + // Remove from the vector + entities.erase(it); + + return true; + } + + return false; +} + +bool Engine::RemoveEntity(const std::string& name) { + Entity* entity = GetEntity(name); + if (entity) { + return RemoveEntity(entity); + } + return false; +} + +std::vector Engine::GetAllEntities() const { + std::vector result; + result.reserve(entities.size()); + + for (const auto& entity : entities) { + result.push_back(entity.get()); + } + + return result; +} + +void Engine::SetActiveCamera(CameraComponent* cameraComponent) { + activeCamera = cameraComponent; +} + +CameraComponent* Engine::GetActiveCamera() const { + return activeCamera; +} + +ResourceManager* Engine::GetResourceManager() const { + return resourceManager.get(); +} + +Platform* Engine::GetPlatform() const { + return platform.get(); +} + +Renderer* Engine::GetRenderer() const { + return renderer.get(); +} + +ModelLoader* Engine::GetModelLoader() const { + return modelLoader.get(); +} + +AudioSystem* Engine::GetAudioSystem() const { + return audioSystem.get(); +} + +PhysicsSystem* Engine::GetPhysicsSystem() const { + return physicsSystem.get(); +} + +ImGuiSystem* Engine::GetImGuiSystem() const { + return imguiSystem.get(); +} + +void Engine::Update(float deltaTime) { + // Update physics system + physicsSystem->Update(deltaTime); + + // Update audio system + audioSystem->Update(deltaTime); + + // Update ImGui system + imguiSystem->NewFrame(); + + // Update all entities + for (auto& entity : entities) { + if (entity->IsActive()) { + entity->Update(deltaTime); + } + } +} + +void Engine::Render() { + // Check if we have an active camera + if (!activeCamera) { + return; + } + + // Get all active entities + std::vector activeEntities; + for (auto& entity : entities) { + if (entity->IsActive()) { + activeEntities.push_back(entity.get()); + } + } + + // Render the scene + renderer->Render(activeEntities, activeCamera); + + // Render ImGui + imguiSystem->Render(renderer->GetCurrentCommandBuffer()); +} + +float Engine::CalculateDeltaTime() { + // Get current time + auto currentTime = static_cast(std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch() + ).count()) / 1000.0f; + + // Calculate delta time + float delta = currentTime - lastFrameTime; + + // Update last frame time + lastFrameTime = currentTime; + + return delta; +} + +void Engine::HandleResize(int width, int height) { + // Update the active camera's aspect ratio + if (activeCamera) { + activeCamera->SetAspectRatio(static_cast(width) / static_cast(height)); + } +} + +#if PLATFORM_ANDROID +// Android-specific implementation +bool Engine::InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers) { + // Create platform + platform = CreatePlatform(app); + if (!platform->Initialize(appName, 0, 0)) { + return false; + } + + // Set resize callback + platform->SetResizeCallback([this](int width, int height) { + HandleResize(width, height); + }); + + // Set mouse callback + platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { + if (imguiSystem) { + imguiSystem->HandleMouse(x, y, buttons); + } + }); + + // Set keyboard callback + platform->SetKeyboardCallback([this](uint32_t key, bool pressed) { + if (imguiSystem) { + imguiSystem->HandleKeyboard(key, pressed); + } + }); + + // Set char callback + platform->SetCharCallback([this](uint32_t c) { + if (imguiSystem) { + imguiSystem->HandleChar(c); + } + }); + + // Create renderer + renderer = std::make_unique(platform.get()); + if (!renderer->Initialize(appName, enableValidationLayers)) { + return false; + } + + // Initialize model loader + if (!modelLoader->Initialize(renderer.get())) { + return false; + } + + // Initialize audio system + if (!audioSystem->Initialize()) { + return false; + } + + // Initialize physics system + if (!physicsSystem->Initialize()) { + return false; + } + + // Get window dimensions from platform + int width, height; + platform->GetWindowSize(&width, &height); + + // Initialize ImGui system + if (!imguiSystem->Initialize(renderer.get(), width, height)) { + return false; + } + + initialized = true; + return true; +} + +void Engine::RunAndroid() { + if (!initialized) { + throw std::runtime_error("Engine not initialized"); + } + + running = true; + + // Main loop is handled by the platform + // We just need to update and render when the platform is ready + + // Calculate delta time + deltaTime = CalculateDeltaTime(); + + // Update + Update(deltaTime); + + // Render + Render(); +} +#endif diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h new file mode 100644 index 00000000..c60ef32e --- /dev/null +++ b/attachments/simple_engine/engine.h @@ -0,0 +1,208 @@ +#pragma once + +#include +#include +#include +#include + +#include "platform.h" +#include "renderer.h" +#include "resource_manager.h" +#include "entity.h" +#include "camera_component.h" +#include "model_loader.h" +#include "audio_system.h" +#include "physics_system.h" +#include "imgui_system.h" + +/** + * @brief Main engine class that manages the game loop and subsystems. + * + * This class implements the core engine architecture as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + */ +class Engine { +public: + /** + * @brief Default constructor. + */ + Engine(); + + /** + * @brief Destructor for proper cleanup. + */ + ~Engine(); + + /** + * @brief Initialize the engine. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @param enableValidationLayers Whether to enable Vulkan validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, int width, int height, bool enableValidationLayers = true); + + /** + * @brief Run the main game loop. + */ + void Run(); + + /** + * @brief Clean up engine resources. + */ + void Cleanup(); + + /** + * @brief Create a new entity. + * @param name The name of the entity. + * @return A pointer to the newly created entity. + */ + Entity* CreateEntity(const std::string& name); + + /** + * @brief Get an entity by name. + * @param name The name of the entity. + * @return A pointer to the entity, or nullptr if not found. + */ + Entity* GetEntity(const std::string& name); + + /** + * @brief Remove an entity. + * @param entity The entity to remove. + * @return True if the entity was removed, false otherwise. + */ + bool RemoveEntity(Entity* entity); + + /** + * @brief Remove an entity by name. + * @param name The name of the entity to remove. + * @return True if the entity was removed, false otherwise. + */ + bool RemoveEntity(const std::string& name); + + /** + * @brief Get all entities. + * @return A vector of pointers to all entities. + */ + std::vector GetAllEntities() const; + + /** + * @brief Set the active camera. + * @param cameraComponent The camera component to set as active. + */ + void SetActiveCamera(CameraComponent* cameraComponent); + + /** + * @brief Get the active camera. + * @return A pointer to the active camera component, or nullptr if none is set. + */ + CameraComponent* GetActiveCamera() const; + + /** + * @brief Get the resource manager. + * @return A pointer to the resource manager. + */ + ResourceManager* GetResourceManager() const; + + /** + * @brief Get the platform. + * @return A pointer to the platform. + */ + Platform* GetPlatform() const; + + /** + * @brief Get the renderer. + * @return A pointer to the renderer. + */ + Renderer* GetRenderer() const; + + /** + * @brief Get the model loader. + * @return A pointer to the model loader. + */ + ModelLoader* GetModelLoader() const; + + /** + * @brief Get the audio system. + * @return A pointer to the audio system. + */ + AudioSystem* GetAudioSystem() const; + + /** + * @brief Get the physics system. + * @return A pointer to the physics system. + */ + PhysicsSystem* GetPhysicsSystem() const; + + /** + * @brief Get the ImGui system. + * @return A pointer to the ImGui system. + */ + ImGuiSystem* GetImGuiSystem() const; + +#if PLATFORM_ANDROID + /** + * @brief Initialize the engine for Android. + * @param app The Android app. + * @param appName The name of the application. + * @param enableValidationLayers Whether to enable Vulkan validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers = true); + + /** + * @brief Run the engine on Android. + */ + void RunAndroid(); +#endif + +private: + // Subsystems + std::unique_ptr platform; + std::unique_ptr renderer; + std::unique_ptr resourceManager; + std::unique_ptr modelLoader; + std::unique_ptr audioSystem; + std::unique_ptr physicsSystem; + std::unique_ptr imguiSystem; + + // Entities + std::vector> entities; + std::unordered_map entityMap; + + // Active camera + CameraComponent* activeCamera = nullptr; + + // Engine state + bool initialized = false; + bool running = false; + + // Delta time calculation + float deltaTime = 0.0f; + float lastFrameTime = 0.0f; + + /** + * @brief Update the engine state. + * @param deltaTime The time elapsed since the last update. + */ + void Update(float deltaTime); + + /** + * @brief Render the scene. + */ + void Render(); + + /** + * @brief Calculate the delta time between frames. + * @return The delta time in seconds. + */ + float CalculateDeltaTime(); + + /** + * @brief Handle window resize events. + * @param width The new width of the window. + * @param height The new height of the window. + */ + void HandleResize(int width, int height); +}; diff --git a/attachments/simple_engine/entity.cpp b/attachments/simple_engine/entity.cpp new file mode 100644 index 00000000..f9dd4860 --- /dev/null +++ b/attachments/simple_engine/entity.cpp @@ -0,0 +1,30 @@ +#include "entity.h" + +// Most of the Entity class implementation is in the header file +// This file is mainly for any methods that might need additional implementation + +void Entity::Initialize() { + for (auto& component : components) { + component->Initialize(); + } +} + +void Entity::Update(float deltaTime) { + if (!active) return; + + for (auto& component : components) { + if (component->IsActive()) { + component->Update(deltaTime); + } + } +} + +void Entity::Render() { + if (!active) return; + + for (auto& component : components) { + if (component->IsActive()) { + component->Render(); + } + } +} diff --git a/attachments/simple_engine/entity.h b/attachments/simple_engine/entity.h new file mode 100644 index 00000000..2911298c --- /dev/null +++ b/attachments/simple_engine/entity.h @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "component.h" + +/** + * @brief Entity class that can have multiple components attached to it. + * + * Entities are containers for components. They don't have any behavior + * on their own, but gain functionality through the components attached to them. + */ +class Entity { +private: + std::string name; + bool active = true; + std::vector> components; + std::unordered_map componentMap; + +public: + /** + * @brief Constructor with a name. + * @param entityName The name of the entity. + */ + explicit Entity(const std::string& entityName) : name(entityName) {} + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Entity() = default; + + /** + * @brief Get the name of the entity. + * @return The name of the entity. + */ + const std::string& GetName() const { return name; } + + /** + * @brief Check if the entity is active. + * @return True if the entity is active, false otherwise. + */ + bool IsActive() const { return active; } + + /** + * @brief Set the active state of the entity. + * @param isActive The new active state. + */ + void SetActive(bool isActive) { active = isActive; } + + /** + * @brief Initialize all components of the entity. + */ + void Initialize(); + + /** + * @brief Update all components of the entity. + * @param deltaTime The time elapsed since the last frame. + */ + void Update(float deltaTime); + + /** + * @brief Render all components of the entity. + */ + void Render(); + + /** + * @brief Add a component to the entity. + * @tparam T The type of component to add. + * @tparam Args The types of arguments to pass to the component constructor. + * @param args The arguments to pass to the component constructor. + * @return A pointer to the newly created component. + */ + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + + // Create the component + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + + // Set the owner + componentPtr->SetOwner(this); + + // Add to the map for quick lookup + componentMap[std::type_index(typeid(T))] = componentPtr; + + // Add to the vector for ownership + components.push_back(std::move(component)); + + // Initialize the component + componentPtr->Initialize(); + + return componentPtr; + } + + /** + * @brief Get a component of a specific type. + * @tparam T The type of component to get. + * @return A pointer to the component, or nullptr if not found. + */ + template + T* GetComponent() const { + static_assert(std::is_base_of::value, "T must derive from Component"); + + auto it = componentMap.find(std::type_index(typeid(T))); + if (it != componentMap.end()) { + return static_cast(it->second); + } + + return nullptr; + } + + /** + * @brief Remove a component of a specific type. + * @tparam T The type of component to remove. + * @return True if the component was removed, false otherwise. + */ + template + bool RemoveComponent() { + static_assert(std::is_base_of::value, "T must derive from Component"); + + auto it = componentMap.find(std::type_index(typeid(T))); + if (it != componentMap.end()) { + Component* componentPtr = it->second; + + // Remove from the map + componentMap.erase(it); + + // Find and remove from the vector + auto vecIt = std::find_if(components.begin(), components.end(), + [componentPtr](const std::unique_ptr& comp) { + return comp.get() == componentPtr; + }); + + if (vecIt != components.end()) { + components.erase(vecIt); + return true; + } + } + + return false; + } + + /** + * @brief Check if the entity has a component of a specific type. + * @tparam T The type of component to check for. + * @return True if the entity has the component, false otherwise. + */ + template + bool HasComponent() const { + static_assert(std::is_base_of::value, "T must derive from Component"); + return componentMap.find(std::type_index(typeid(T))) != componentMap.end(); + } +}; diff --git a/attachments/simple_engine/imgui/imconfig.h b/attachments/simple_engine/imgui/imconfig.h new file mode 100644 index 00000000..33cbadd1 --- /dev/null +++ b/attachments/simple_engine/imgui/imconfig.h @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +// USER IMPLEMENTATION +// This file contains compile-time options for ImGui. +// Other options (memory allocation overrides, callbacks, etc.) can be set at runtime via the ImGuiIO structure - ImGui::GetIO(). +//----------------------------------------------------------------------------- + +#pragma once + +//---- Define assertion handler. Defaults to calling assert(). +//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) + +//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows. +//#define IMGUI_API __declspec( dllexport ) +//#define IMGUI_API __declspec( dllimport ) + +//---- Include imgui_user.h at the end of imgui.h +//#define IMGUI_INCLUDE_IMGUI_USER_H + +//---- Don't implement default handlers for Windows (so as not to link with OpenClipboard() and others Win32 functions) +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS + +//---- Don't implement help and test window functionality (ShowUserGuide()/ShowStyleEditor()/ShowTestWindow() methods will be empty) +//#define IMGUI_DISABLE_TEST_WINDOWS + +//---- Don't define obsolete functions names +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//---- Implement STB libraries in a namespace to avoid conflicts +//#define IMGUI_STB_NAMESPACE ImGuiStb + +//---- Define constructor and implicit cast operators to convert back<>forth from your math types and ImVec2/ImVec4. +/* +#define IM_VEC2_CLASS_EXTRA \ + ImVec2(const MyVec2& f) { x = f.x; y = f.y; } \ + operator MyVec2() const { return MyVec2(x,y); } + +#define IM_VEC4_CLASS_EXTRA \ + ImVec4(const MyVec4& f) { x = f.x; y = f.y; z = f.z; w = f.w; } \ + operator MyVec4() const { return MyVec4(x,y,z,w); } +*/ + +//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. +//---- e.g. create variants of the ImGui::Value() helper for your low-level math types, or your own widgets/helpers. +/* +namespace ImGui +{ + void Value(const char* prefix, const MyMatrix44& v, const char* float_format = NULL); +} +*/ + diff --git a/attachments/simple_engine/imgui/imgui.cpp b/attachments/simple_engine/imgui/imgui.cpp new file mode 100644 index 00000000..c53edb84 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui.cpp @@ -0,0 +1,13278 @@ +// dear imgui, v1.60 WIP +// (main code and documentation) + +// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. +// Newcomers, read 'Programmer guide' below for notes on how to setup Dear ImGui in your codebase. +// Get latest version at https://github.com/ocornut/imgui +// Releases change-log at https://github.com/ocornut/imgui/releases +// Gallery (please post your screenshots/video there!): https://github.com/ocornut/imgui/issues/1269 +// Developed by Omar Cornut and every direct or indirect contributors to the GitHub. +// This library is free but I need your support to sustain development and maintenance. +// If you work for a company, please consider financial support, see Readme. For individuals: https://www.patreon.com/imgui + +/* + + Index + - MISSION STATEMENT + - END-USER GUIDE + - PROGRAMMER GUIDE (read me!) + - Read first + - How to update to a newer version of Dear ImGui + - Getting started with integrating Dear ImGui in your code/engine + - Using gamepad/keyboard navigation [BETA] + - API BREAKING CHANGES (read me when you update!) + - ISSUES & TODO LIST + - FREQUENTLY ASKED QUESTIONS (FAQ), TIPS + - How can I help? + - How can I display an image? What is ImTextureID, how does it works? + - How can I have multiple widgets with the same label? Can I have widget without a label? (Yes). A primer on labels and the ID stack. + - How can I tell when Dear ImGui wants my mouse/keyboard inputs VS when I can pass them to my application? + - How can I load a different font than the default? + - How can I easily use icons in my application? + - How can I load multiple fonts? + - How can I display and input non-latin characters such as Chinese, Japanese, Korean, Cyrillic? + - How can I preserve my Dear ImGui context across reloading a DLL? (loss of the global/static variables) + - How can I use the drawing facilities without an ImGui window? (using ImDrawList API) + - I integrated Dear ImGui in my engine and the text or lines are blurry.. + - I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. + - ISSUES & TODO-LIST + - CODE + + + MISSION STATEMENT + ================= + + - Easy to use to create code-driven and data-driven tools + - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools + - Easy to hack and improve + - Minimize screen real-estate usage + - Minimize setup and maintenance + - Minimize state storage on user side + - Portable, minimize dependencies, run on target (consoles, phones, etc.) + - Efficient runtime and memory consumption (NB- we do allocate when "growing" content e.g. creating a window, opening a tree node + for the first time, etc. but a typical frame won't allocate anything) + + Designed for developers and content-creators, not the typical end-user! Some of the weaknesses includes: + - Doesn't look fancy, doesn't animate + - Limited layout features, intricate layouts are typically crafted in code + + + END-USER GUIDE + ============== + + - Double-click on title bar to collapse window. + - Click upper right corner to close a window, available when 'bool* p_open' is passed to ImGui::Begin(). + - Click and drag on lower right corner to resize window (double-click to auto fit window to its contents). + - Click and drag on any empty space to move window. + - TAB/SHIFT+TAB to cycle through keyboard editable fields. + - CTRL+Click on a slider or drag box to input value as text. + - Use mouse wheel to scroll. + - Text editor: + - Hold SHIFT or use mouse to select text. + - CTRL+Left/Right to word jump. + - CTRL+Shift+Left/Right to select words. + - CTRL+A our Double-Click to select all. + - CTRL+X,CTRL+C,CTRL+V to use OS clipboard/ + - CTRL+Z,CTRL+Y to undo/redo. + - ESCAPE to revert text to its original value. + - You can apply arithmetic operators +,*,/ on numerical values. Use +- to subtract (because - would set a negative value!) + - Controls are automatically adjusted for OSX to match standard OSX text editing operations. + - Gamepad navigation: see suggested mappings in imgui.h ImGuiNavInput_ + + + PROGRAMMER GUIDE + ================ + + READ FIRST + + - Read the FAQ below this section! + - Your code creates the UI, if your code doesn't run the UI is gone! == very dynamic UI, no construction/destructions steps, less data retention + on your side, no state duplication, less sync, less bugs. + - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. + - You can learn about immediate-mode gui principles at http://www.johno.se/book/imgui.html or watch http://mollyrocket.com/861 + + HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + + - Overwrite all the sources files except for imconfig.h (if you have made modification to your copy of imconfig.h) + - Read the "API BREAKING CHANGES" section (below). This is where we list occasional API breaking changes. + If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed from the public API. + If you have a problem with a missing function/symbols, search for its name in the code, there will likely be a comment about it. + Please report any issue to the GitHub page! + - Try to keep your copy of dear imgui reasonably up to date. + + GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + + - Add the Dear ImGui source files to your projects, using your preferred build system. + It is recommended you build the .cpp files as part of your project and not as a library. + - You can later customize the imconfig.h file to tweak some compilation time behavior, such as integrating imgui types with your own maths types. + - See examples/ folder for standalone sample applications. + - You may be able to grab and copy a ready made imgui_impl_*** file from the examples/. + - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them. + + - Init: retrieve the ImGuiIO structure with ImGui::GetIO() and fill the fields marked 'Settings': at minimum you need to set io.DisplaySize + (application resolution). Later on you will fill your keyboard mapping, clipboard handlers, and other advanced features but for a basic + integration you don't need to worry about it all. + - Init: call io.Fonts->GetTexDataAsRGBA32(...), it will build the font atlas texture, then load the texture pixels into graphics memory. + - Every frame: + - In your main loop as early a possible, fill the IO fields marked 'Input' (e.g. mouse position, buttons, keyboard info, etc.) + - Call ImGui::NewFrame() to begin the frame + - You can use any ImGui function you want between NewFrame() and Render() + - Call ImGui::Render() as late as you can to end the frame and finalize render data. it will call your io.RenderDrawListFn handler. + (Even if you don't render, call Render() and ignore the callback, or call EndFrame() instead. Otherwhise some features will break) + - All rendering information are stored into command-lists until ImGui::Render() is called. + - Dear ImGui never touches or knows about your GPU state. the only function that knows about GPU is the RenderDrawListFn handler that you provide. + - Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" phases + of your own application. + - Refer to the examples applications in the examples/ folder for instruction on how to setup your code. + - A minimal application skeleton may be: + + // Application init + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x = 1920.0f; + io.DisplaySize.y = 1280.0f; + // TODO: Fill others settings of the io structure later. + + // Load texture atlas (there is a default font so you don't need to care about choosing a font yet) + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(pixels, &width, &height); + // TODO: At this points you've got the texture data and you need to upload that your your graphic system: + MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA) + // TODO: Store your texture pointer/identifier (whatever your engine uses) in 'io.Fonts->TexID'. This will be passed back to your via the renderer. + io.Fonts->TexID = (void*)texture; + + // Application main loop + while (true) + { + // Setup low-level inputs (e.g. on Win32, GetKeyboardState(), or write to those fields from your Windows message loop handlers, etc.) + ImGuiIO& io = ImGui::GetIO(); + io.DeltaTime = 1.0f/60.0f; + io.MousePos = mouse_pos; + io.MouseDown[0] = mouse_button_0; + io.MouseDown[1] = mouse_button_1; + + // Call NewFrame(), after this point you can use ImGui::* functions anytime + ImGui::NewFrame(); + + // Most of your application code here + MyGameUpdate(); // may use any ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End(); + MyGameRender(); // may use any ImGui functions as well! + + // Render & swap video buffers + ImGui::Render(); + MyImGuiRenderFunction(ImGui::GetDrawData()); + SwapBuffers(); + } + + // Shutdown + ImGui::DestroyContext(); + + + - A minimal render function skeleton may be: + + void void MyRenderFunction(ImDrawData* draw_data) + { + // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled + // TODO: Setup viewport, orthographic projection matrix + // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color. + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; // vertex buffer generated by ImGui + const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; // index buffer generated by ImGui + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback) + { + pcmd->UserCallback(cmd_list, pcmd); + } + else + { + // The texture for the draw call is specified by pcmd->TextureId. + // The vast majority of draw calls with use the imgui texture atlas, which value you have set yourself during initialization. + MyEngineBindTexture(pcmd->TextureId); + + // We are using scissoring to clip some objects. All low-level graphics API supports it. + // If your engine doesn't support scissoring yet, you will get some small glitches (some elements outside their bounds) which you can fix later. + MyEngineScissor((int)pcmd->ClipRect.x, (int)pcmd->ClipRect.y, (int)(pcmd->ClipRect.z - pcmd->ClipRect.x), (int)(pcmd->ClipRect.w - pcmd->ClipRect.y)); + + // Render 'pcmd->ElemCount/3' indexed triangles. + // By default the indices ImDrawIdx are 16-bits, you can change them to 32-bits if your engine doesn't support 16-bits indices. + MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer, vtx_buffer); + } + idx_buffer += pcmd->ElemCount; + } + } + } + + - The examples/ folders contains many functional implementation of the pseudo-code above. + - When calling NewFrame(), the 'io.WantCaptureMouse'/'io.WantCaptureKeyboard'/'io.WantTextInput' flags are updated. + They tell you if ImGui intends to use your inputs. So for example, if 'io.WantCaptureMouse' is set you would typically want to hide + mouse inputs from the rest of your application. Read the FAQ below for more information about those flags. + + USING GAMEPAD/KEYBOARD NAVIGATION [BETA] + + - Ask questions and report issues at https://github.com/ocornut/imgui/issues/787 + - The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. + - Keyboard: + - Set io.NavFlags |= ImGuiNavFlags_EnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeyDown[] + io.KeyMap[] arrays. + - When keyboard navigation is active (io.NavActive + NavFlags_EnableKeyboard), the io.WantCaptureKeyboard flag will be set. + For more advanced uses, you may want to read from: + - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. + - io.NavVisible: true when the navigation cursor is visible (and usually goes false when mouse is used). + - or query focus information with e.g. IsWindowFocused(), IsItemFocused() etc. functions. + Please reach out if you think the game vs navigation input sharing could be improved. + - Gamepad: + - Set io.NavFlags |= ImGuiNavFlags_EnableGamepad to enable. Fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). + - See 'enum ImGuiNavInput_' in imgui.h for a description of inputs. For each entry of io.NavInputs[], set the following values: + 0.0f= not held. 1.0f= fully held. Pass intermediate 0.0f..1.0f values for analog triggers/sticks. + - We uses a simple >0.0f test for activation testing, and won't attempt to test for a dead-zone. + Your code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, maybe a power curve, etc.). + - If you need to share inputs between your game and the imgui parts, the easiest approach is to go all-or-nothing, with a buttons combo to toggle the target. + Please reach out if you think the game vs navigation input sharing could be improved. + - Mouse: + - PS4 users: Consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback. + - Consoles/Tablet/Phone users: Consider using Synergy host (on your computer) + uSynergy.c (in your console/tablet/phone app) to use your PC mouse/keyboard. + - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiNavFlags_MoveMouse flag in io.NavFlags. + Enabling ImGuiNavFlags_MoveMouse instructs dear imgui to move your mouse cursor along with navigation movements. + When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantMoveMouse' to notify you that it wants the mouse cursor to be moved. + When that happens your back-end NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the binding in examples/ do that. + (If you set the ImGuiNavFlags_MoveMouse flag but don't honor 'io.WantMoveMouse' properly, imgui will misbehave as it will see your mouse as moving back and forth.) + (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want + to set a boolean to ignore your other external mouse positions until the external source is moved again.) + + + API BREAKING CHANGES + ==================== + + Occasionally introducing changes that are breaking the API. The breakage are generally minor and easy to fix. + Here is a change-log of API breaking changes, if you are using one of the functions listed, expect to have to fix some code. + Also read releases logs https://github.com/ocornut/imgui/releases for more details. + + - 2018/02/18 (1.60) - BeginDragDropSource(): temporarily removed the optional mouse_button=0 parameter because it is not really usable in many situations at the moment. + - 2018/02/16 (1.60) - obsoleted the io.RenderDrawListsFn callback, you can call your graphics engine render function after ImGui::Render(). Use ImGui::GetDrawData() to retrieve the ImDrawData* to display. + - 2018/02/07 (1.60) - reorganized context handling to be more explicit, + - YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. + - removed Shutdown() function, as DestroyContext() serve this purpose. + - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwhise CreateContext() will create its own font atlas instance. + - removed allocator parameters from CreateContext(), they are now setup with SetAllocatorFunctions(), and shared by all contexts. + - removed the default global context and font atlas instance, which were confusing for users of DLL reloading and users of multiple contexts. + - 2018/01/31 (1.60) - moved sample TTF files from extra_fonts/ to misc/fonts/. If you loaded files directly from the imgui repo you may need to update your paths. + - 2018/01/11 (1.60) - obsoleted IsAnyWindowHovered() in favor of IsWindowHovered(ImGuiHoveredFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/11 (1.60) - obsoleted IsAnyWindowFocused() in favor of IsWindowFocused(ImGuiFocusedFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/03 (1.60) - renamed ImGuiSizeConstraintCallback to ImGuiSizeCallback, ImGuiSizeConstraintCallbackData to ImGuiSizeCallbackData. + - 2017/12/29 (1.60) - removed CalcItemRectClosestPoint() which was weird and not really used by anyone except demo code. If you need it it's easy to replicate on your side. + - 2017/12/24 (1.53) - renamed the emblematic ShowTestWindow() function to ShowDemoWindow(). Kept redirection function (will obsolete). + - 2017/12/21 (1.53) - ImDrawList: renamed style.AntiAliasedShapes to style.AntiAliasedFill for consistency and as a way to explicitly break code that manipulate those flag at runtime. You can now manipulate ImDrawList::Flags + - 2017/12/21 (1.53) - ImDrawList: removed 'bool anti_aliased = true' final parameter of ImDrawList::AddPolyline() and ImDrawList::AddConvexPolyFilled(). Prefer manipulating ImDrawList::Flags if you need to toggle them during the frame. + - 2017/12/14 (1.53) - using the ImGuiWindowFlags_NoScrollWithMouse flag on a child window forwards the mouse wheel event to the parent window, unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set. + - 2017/12/13 (1.53) - renamed GetItemsLineHeightWithSpacing() to GetFrameHeightWithSpacing(). Kept redirection function (will obsolete). + - 2017/12/13 (1.53) - obsoleted IsRootWindowFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootWindow). Kept redirection function (will obsolete). + - obsoleted IsRootWindowOrAnyChildFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows). Kept redirection function (will obsolete). + - 2017/12/12 (1.53) - renamed ImGuiTreeNodeFlags_AllowOverlapMode to ImGuiTreeNodeFlags_AllowItemOverlap. Kept redirection enum (will obsolete). + - 2017/12/10 (1.53) - removed SetNextWindowContentWidth(), prefer using SetNextWindowContentSize(). Kept redirection function (will obsolete). + - 2017/11/27 (1.53) - renamed ImGuiTextBuffer::append() helper to appendf(), appendv() to appendfv(). If you copied the 'Log' demo in your code, it uses appendv() so that needs to be renamed. + - 2017/11/18 (1.53) - Style, Begin: removed ImGuiWindowFlags_ShowBorders window flag. Borders are now fully set up in the ImGuiStyle structure (see e.g. style.FrameBorderSize, style.WindowBorderSize). Use ImGui::ShowStyleEditor() to look them up. + Please note that the style system will keep evolving (hopefully stabilizing in Q1 2018), and so custom styles will probably subtly break over time. It is recommended you use the StyleColorsClassic(), StyleColorsDark(), StyleColorsLight() functions. + - 2017/11/18 (1.53) - Style: removed ImGuiCol_ComboBg in favor of combo boxes using ImGuiCol_PopupBg for consistency. + - 2017/11/18 (1.53) - Style: renamed ImGuiCol_ChildWindowBg to ImGuiCol_ChildBg. + - 2017/11/18 (1.53) - Style: renamed style.ChildWindowRounding to style.ChildRounding, ImGuiStyleVar_ChildWindowRounding to ImGuiStyleVar_ChildRounding. + - 2017/11/02 (1.53) - obsoleted IsRootWindowOrAnyChildHovered() in favor of using IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); + - 2017/10/24 (1.52) - renamed IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS to IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS for consistency. + - 2017/10/20 (1.52) - changed IsWindowHovered() default parameters behavior to return false if an item is active in another window (e.g. click-dragging item from another window to this window). You can use the newly introduced IsWindowHovered() flags to requests this specific behavior if you need it. + - 2017/10/20 (1.52) - marked IsItemHoveredRect()/IsMouseHoveringWindow() as obsolete, in favor of using the newly introduced flags for IsItemHovered() and IsWindowHovered(). See https://github.com/ocornut/imgui/issues/1382 for details. + removed the IsItemRectHovered()/IsWindowRectHovered() names introduced in 1.51 since they were merely more consistent names for the two functions we are now obsoleting. + - 2017/10/17 (1.52) - marked the old 5-parameters version of Begin() as obsolete (still available). Use SetNextWindowSize()+Begin() instead! + - 2017/10/11 (1.52) - renamed AlignFirstTextHeightToWidgets() to AlignTextToFramePadding(). Kept inline redirection function (will obsolete). + - 2017/09/25 (1.52) - removed SetNextWindowPosCenter() because SetNextWindowPos() now has the optional pivot information to do the same and more. Kept redirection function (will obsolete). + - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your binding if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". + - 2017/08/22 (1.51) - renamed IsItemHoveredRect() to IsItemRectHovered(). Kept inline redirection function (will obsolete). -> (1.52) use IsItemHovered(ImGuiHoveredFlags_RectOnly)! + - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete). + - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete). + - 2017/08/20 (1.51) - renamed GetStyleColName() to GetStyleColorName() for consistency. + - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicily to fix. + - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame. + - 2017/08/15 (1.51) - changed parameter order for BeginPopupContextWindow() from (const char*,int buttons,bool also_over_items) to (const char*,int buttons,bool also_over_items). Note that most calls relied on default parameters completely. + - 2017/08/13 (1.51) - renamed ImGuiCol_Columns*** to ImGuiCol_Separator***. Kept redirection enums (will obsolete). + - 2017/08/11 (1.51) - renamed ImGuiSetCond_*** types and flags to ImGuiCond_***. Kept redirection enums (will obsolete). + - 2017/08/09 (1.51) - removed ValueColor() helpers, they are equivalent to calling Text(label) + SameLine() + ColorButton(). + - 2017/08/08 (1.51) - removed ColorEditMode() and ImGuiColorEditMode in favor of ImGuiColorEditFlags and parameters to the various Color*() functions. The SetColorEditOptions() allows to initialize default but the user can still change them with right-click context menu. + - changed prototype of 'ColorEdit4(const char* label, float col[4], bool show_alpha = true)' to 'ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)', where passing flags = 0x01 is a safe no-op (hello dodgy backward compatibility!). - check and run the demo window, under "Color/Picker Widgets", to understand the various new options. + - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0))' + - 2017/07/20 (1.51) - removed IsPosHoveringAnyWindow(ImVec2), which was partly broken and misleading. ASSERT + redirect user to io.WantCaptureMouse + - 2017/05/26 (1.50) - removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. + - 2017/05/01 (1.50) - renamed ImDrawList::PathFill() (rarely used directly) to ImDrawList::PathFillConvex() for clarity. + - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetId() and use it instead of passing string to BeginChild(). + - 2016/10/15 (1.50) - avoid 'void* user_data' parameter to io.SetClipboardTextFn/io.GetClipboardTextFn pointers. We pass io.ClipboardUserData to it. + - 2016/09/25 (1.50) - style.WindowTitleAlign is now a ImVec2 (ImGuiAlign enum was removed). set to (0.5f,0.5f) for horizontal+vertical centering, (0.0f,0.0f) for upper-left, etc. + - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully breakage should be minimal. + - 2016/05/12 (1.49) - title bar (using ImGuiCol_TitleBg/ImGuiCol_TitleBgActive colors) isn't rendered over a window background (ImGuiCol_WindowBg color) anymore. + If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you. + However if your TitleBg/TitleBgActive alpha was <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color. + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) + { + float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; + return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); + } + If this is confusing, pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color. + - 2016/05/07 (1.49) - removed confusing set of GetInternalState(), GetInternalStateSize(), SetInternalState() functions. Now using CreateContext(), DestroyContext(), GetCurrentContext(), SetCurrentContext(). + - 2016/05/02 (1.49) - renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(), no redirection. + - 2016/05/01 (1.49) - obsoleted old signature of CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false) as extra parameters were badly designed and rarely used. You can replace the "default_open = true" flag in new API with CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen). + - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDraw::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. + - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref github issue #337). + - 2016/04/03 (1.48) - renamed ImGuiCol_TooltipBg to ImGuiCol_PopupBg, used by popups/menus and tooltips. popups/menus were previously using ImGuiCol_WindowBg. (ref github issue #337) + - 2016/03/21 (1.48) - renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). + - 2016/03/02 (1.48) - InputText() completion/history/always callbacks: if you modify the text buffer manually (without using DeleteChars()/InsertChars() helper) you need to maintain the BufTextLen field. added an assert. + - 2016/01/23 (1.48) - fixed not honoring exact width passed to PushItemWidth(), previously it would add extra FramePadding.x*2 over that width. if you had manual pixel-perfect alignment in place it might affect you. + - 2015/12/27 (1.48) - fixed ImDrawList::AddRect() which used to render a rectangle 1 px too large on each axis. + - 2015/12/04 (1.47) - renamed Color() helpers to ValueColor() - dangerously named, rarely used and probably to be made obsolete. + - 2015/08/29 (1.45) - with the addition of horizontal scrollbar we made various fixes to inconsistencies with dealing with cursor position. + GetCursorPos()/SetCursorPos() functions now include the scrolled amount. It shouldn't affect the majority of users, but take note that SetCursorPosX(100.0f) puts you at +100 from the starting x position which may include scrolling, not at +100 from the window left side. + GetContentRegionMax()/GetWindowContentRegionMin()/GetWindowContentRegionMax() functions allow include the scrolled amount. Typically those were used in cases where no scrolling would happen so it may not be a problem, but watch out! + - 2015/08/29 (1.45) - renamed style.ScrollbarWidth to style.ScrollbarSize + - 2015/08/05 (1.44) - split imgui.cpp into extra files: imgui_demo.cpp imgui_draw.cpp imgui_internal.h that you need to add to your project. + - 2015/07/18 (1.44) - fixed angles in ImDrawList::PathArcTo(), PathArcToFast() (introduced in 1.43) being off by an extra PI for no justifiable reason + - 2015/07/14 (1.43) - add new ImFontAtlas::AddFont() API. For the old AddFont***, moved the 'font_no' parameter of ImFontAtlas::AddFont** functions to the ImFontConfig structure. + you need to render your textured triangles with bilinear filtering to benefit from sub-pixel positioning of text. + - 2015/07/08 (1.43) - switched rendering data to use indexed rendering. this is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. + this necessary change will break your rendering function! the fix should be very easy. sorry for that :( + - if you are using a vanilla copy of one of the imgui_impl_XXXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. + - the signature of the io.RenderDrawListsFn handler has changed! + ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) + became: + ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). + argument 'cmd_lists' -> 'draw_data->CmdLists' + argument 'cmd_lists_count' -> 'draw_data->CmdListsCount' + ImDrawList 'commands' -> 'CmdBuffer' + ImDrawList 'vtx_buffer' -> 'VtxBuffer' + ImDrawList n/a -> 'IdxBuffer' (new) + ImDrawCmd 'vtx_count' -> 'ElemCount' + ImDrawCmd 'clip_rect' -> 'ClipRect' + ImDrawCmd 'user_callback' -> 'UserCallback' + ImDrawCmd 'texture_id' -> 'TextureId' + - each ImDrawList now contains both a vertex buffer and an index buffer. For each command, render ElemCount/3 triangles using indices from the index buffer. + - if you REALLY cannot render indexed primitives, you can call the draw_data->DeIndexAllBuffers() method to de-index the buffers. This is slow and a waste of CPU/GPU. Prefer using indexed rendering! + - refer to code in the examples/ folder or ask on the GitHub if you are unsure of how to upgrade. please upgrade! + - 2015/07/10 (1.43) - changed SameLine() parameters from int to float. + - 2015/07/02 (1.42) - renamed SetScrollPosHere() to SetScrollFromCursorPos(). Kept inline redirection function (will obsolete). + - 2015/07/02 (1.42) - renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion along with other scrolling functions, because positions (e.g. cursor position) are not equivalent to scrolling amount. + - 2015/06/14 (1.41) - changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent) - makes a difference when texture have transparence + - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely be used. Sorry! + - 2015/05/31 (1.40) - renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline redirection function (will obsolete). + - 2015/05/31 (1.40) - renamed IsRectClipped() to IsRectVisible() for consistency. Note that return value is opposite! Kept inline redirection function (will obsolete). + - 2015/05/27 (1.40) - removed the third 'repeat_if_held' parameter from Button() - sorry! it was rarely used and inconsistent. Use PushButtonRepeat(true) / PopButtonRepeat() to enable repeat on desired buttons. + - 2015/05/11 (1.40) - changed BeginPopup() API, takes a string identifier instead of a bool. ImGui needs to manage the open/closed state of popups. Call OpenPopup() to actually set the "open" state of a popup. BeginPopup() returns true if the popup is opened. + - 2015/05/03 (1.40) - removed style.AutoFitPadding, using style.WindowPadding makes more sense (the default values were already the same). + - 2015/04/13 (1.38) - renamed IsClipped() to IsRectClipped(). Kept inline redirection function until 1.50. + - 2015/04/09 (1.38) - renamed ImDrawList::AddArc() to ImDrawList::AddArcFast() for compatibility with future API + - 2015/04/03 (1.38) - removed ImGuiCol_CheckHovered, ImGuiCol_CheckActive, replaced with the more general ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive. + - 2014/04/03 (1.38) - removed support for passing -FLT_MAX..+FLT_MAX as the range for a SliderFloat(). Use DragFloat() or Inputfloat() instead. + - 2015/03/17 (1.36) - renamed GetItemBoxMin()/GetItemBoxMax()/IsMouseHoveringBox() to GetItemRectMin()/GetItemRectMax()/IsMouseHoveringRect(). Kept inline redirection function until 1.50. + - 2015/03/15 (1.36) - renamed style.TreeNodeSpacing to style.IndentSpacing, ImGuiStyleVar_TreeNodeSpacing to ImGuiStyleVar_IndentSpacing + - 2015/03/13 (1.36) - renamed GetWindowIsFocused() to IsWindowFocused(). Kept inline redirection function until 1.50. + - 2015/03/08 (1.35) - renamed style.ScrollBarWidth to style.ScrollbarWidth (casing) + - 2015/02/27 (1.34) - renamed OpenNextNode(bool) to SetNextTreeNodeOpened(bool, ImGuiSetCond). Kept inline redirection function until 1.50. + - 2015/02/27 (1.34) - renamed ImGuiSetCondition_*** to ImGuiSetCond_***, and _FirstUseThisSession becomes _Once. + - 2015/02/11 (1.32) - changed text input callback ImGuiTextEditCallback return type from void-->int. reserved for future use, return 0 for now. + - 2015/02/10 (1.32) - renamed GetItemWidth() to CalcItemWidth() to clarify its evolving behavior + - 2015/02/08 (1.31) - renamed GetTextLineSpacing() to GetTextLineHeightWithSpacing() + - 2015/02/01 (1.31) - removed IO.MemReallocFn (unused) + - 2015/01/19 (1.30) - renamed ImGuiStorage::GetIntPtr()/GetFloatPtr() to GetIntRef()/GetIntRef() because Ptr was conflicting with actual pointer storage functions. + - 2015/01/11 (1.30) - big font/image API change! now loads TTF file. allow for multiple fonts. no need for a PNG loader. + (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. + this sequence: + const void* png_data; + unsigned int png_size; + ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); + // + became: + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + // + io.Fonts->TexID = (your_texture_identifier); + you now have much more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. + it is now recommended that you sample the font texture with bilinear interpolation. + (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to set io.Fonts->TexID. + (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) + (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets + - 2014/12/10 (1.18) - removed SetNewWindowDefaultPos() in favor of new generic API SetNextWindowPos(pos, ImGuiSetCondition_FirstUseEver) + - 2014/11/28 (1.17) - moved IO.Font*** options to inside the IO.Font-> structure (FontYOffset, FontTexUvForWhite, FontBaseScale, FontFallbackGlyph) + - 2014/11/26 (1.17) - reworked syntax of IMGUI_ONCE_UPON_A_FRAME helper macro to increase compiler compatibility + - 2014/11/07 (1.15) - renamed IsHovered() to IsItemHovered() + - 2014/10/02 (1.14) - renamed IMGUI_INCLUDE_IMGUI_USER_CPP to IMGUI_INCLUDE_IMGUI_USER_INL and imgui_user.cpp to imgui_user.inl (more IDE friendly) + - 2014/09/25 (1.13) - removed 'text_end' parameter from IO.SetClipboardTextFn (the string is now always zero-terminated for simplicity) + - 2014/09/24 (1.12) - renamed SetFontScale() to SetWindowFontScale() + - 2014/09/24 (1.12) - moved IM_MALLOC/IM_REALLOC/IM_FREE preprocessor defines to IO.MemAllocFn/IO.MemReallocFn/IO.MemFreeFn + - 2014/08/30 (1.09) - removed IO.FontHeight (now computed automatically) + - 2014/08/30 (1.09) - moved IMGUI_FONT_TEX_UV_FOR_WHITE preprocessor define to IO.FontTexUvForWhite + - 2014/08/28 (1.09) - changed the behavior of IO.PixelCenterOffset following various rendering fixes + + + ISSUES & TODO-LIST + ================== + See TODO.txt + + + FREQUENTLY ASKED QUESTIONS (FAQ), TIPS + ====================================== + + Q: How can I help? + A: - If you are experienced with Dear ImGui and C++, look at the github issues, or TODO.txt and see how you want/can help! + - Convince your company to fund development time! Individual users: you can also become a Patron (patreon.com/imgui) or donate on PayPal! See README. + - Disclose your usage of dear imgui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. + You may post screenshot or links in the gallery threads (github.com/ocornut/imgui/issues/1269). Visuals are ideal as they inspire other programmers. + But even without visuals, disclosing your use of dear imgui help the library grow credibility, and help other teams and programmers with taking decisions. + - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on github or privately). + + Q: How can I display an image? What is ImTextureID, how does it works? + A: ImTextureID is a void* used to pass renderer-agnostic texture references around until it hits your render function. + Dear ImGui knows nothing about what those bits represent, it just passes them around. It is up to you to decide what you want the void* to carry! + It could be an identifier to your OpenGL texture (cast GLuint to void*), a pointer to your custom engine material (cast MyMaterial* to void*), etc. + At the end of the chain, your renderer takes this void* to cast it back into whatever it needs to select a current texture to render. + Refer to examples applications, where each renderer (in a imgui_impl_xxxx.cpp file) is treating ImTextureID as a different thing. + (c++ tip: OpenGL uses integers to identify textures. You can safely store an integer into a void*, just cast it to void*, don't take it's address!) + To display a custom image/texture within an ImGui window, you may use ImGui::Image(), ImGui::ImageButton(), ImDrawList::AddImage() functions. + Dear ImGui will generate the geometry and draw calls using the ImTextureID that you passed and which your renderer can use. + You may call ImGui::ShowMetricsWindow() to explore active draw lists and visualize/understand how the draw data is generated. + It is your responsibility to get textures uploaded to your GPU. + + Q: Can I have multiple widgets with the same label? Can I have widget without a label? + A: Yes. A primer on labels and the ID stack... + + - Elements that are typically not clickable, such as Text() items don't need an ID. + + - Interactive widgets require state to be carried over multiple frames (most typically Dear ImGui often needs to remember what is + the "active" widget). to do so they need a unique ID. unique ID are typically derived from a string label, an integer index or a pointer. + + Button("OK"); // Label = "OK", ID = hash of "OK" + Button("Cancel"); // Label = "Cancel", ID = hash of "Cancel" + + - ID are uniquely scoped within windows, tree nodes, etc. so no conflict can happen if you have two buttons called "OK" + in two different windows or in two different locations of a tree. + + - If you have a same ID twice in the same location, you'll have a conflict: + + Button("OK"); + Button("OK"); // ID collision! Both buttons will be treated as the same. + + Fear not! this is easy to solve and there are many ways to solve it! + + - When passing a label you can optionally specify extra unique ID information within string itself. + Use "##" to pass a complement to the ID that won't be visible to the end-user. + This helps solving the simple collision cases when you know which items are going to be created. + + Button("Play"); // Label = "Play", ID = hash of "Play" + Button("Play##foo1"); // Label = "Play", ID = hash of "Play##foo1" (different from above) + Button("Play##foo2"); // Label = "Play", ID = hash of "Play##foo2" (different from above) + + - If you want to completely hide the label, but still need an ID: + + Checkbox("##On", &b); // Label = "", ID = hash of "##On" (no label!) + + - Occasionally/rarely you might want change a label while preserving a constant ID. This allows you to animate labels. + For example you may want to include varying information in a window title bar, but windows are uniquely identified by their ID.. + Use "###" to pass a label that isn't part of ID: + + Button("Hello###ID"; // Label = "Hello", ID = hash of "ID" + Button("World###ID"; // Label = "World", ID = hash of "ID" (same as above) + + sprintf(buf, "My game (%f FPS)###MyGame", fps); + Begin(buf); // Variable label, ID = hash of "MyGame" + + - Use PushID() / PopID() to create scopes and avoid ID conflicts within the same Window. + This is the most convenient way of distinguishing ID if you are iterating and creating many UI elements. + You can push a pointer, a string or an integer value. Remember that ID are formed from the concatenation of _everything_ in the ID stack! + + for (int i = 0; i < 100; i++) + { + PushID(i); + Button("Click"); // Label = "Click", ID = hash of integer + "label" (unique) + PopID(); + } + + for (int i = 0; i < 100; i++) + { + MyObject* obj = Objects[i]; + PushID(obj); + Button("Click"); // Label = "Click", ID = hash of pointer + "label" (unique) + PopID(); + } + + for (int i = 0; i < 100; i++) + { + MyObject* obj = Objects[i]; + PushID(obj->Name); + Button("Click"); // Label = "Click", ID = hash of string + "label" (unique) + PopID(); + } + + - More example showing that you can stack multiple prefixes into the ID stack: + + Button("Click"); // Label = "Click", ID = hash of "Click" + PushID("node"); + Button("Click"); // Label = "Click", ID = hash of "node" + "Click" + PushID(my_ptr); + Button("Click"); // Label = "Click", ID = hash of "node" + ptr + "Click" + PopID(); + PopID(); + + - Tree nodes implicitly creates a scope for you by calling PushID(). + + Button("Click"); // Label = "Click", ID = hash of "Click" + if (TreeNode("node")) + { + Button("Click"); // Label = "Click", ID = hash of "node" + "Click" + TreePop(); + } + + - When working with trees, ID are used to preserve the open/close state of each tree node. + Depending on your use cases you may want to use strings, indices or pointers as ID. + e.g. when displaying a single object that may change over time (dynamic 1-1 relationship), using a static string as ID will preserve your + node open/closed state when the targeted object change. + e.g. when displaying a list of objects, using indices or pointers as ID will preserve the node open/closed state differently. + experiment and see what makes more sense! + + Q: How can I tell when Dear ImGui wants my mouse/keyboard inputs VS when I can pass them to my application? + A: You can read the 'io.WantCaptureMouse'/'io.WantCaptureKeyboard'/'ioWantTextInput' flags from the ImGuiIO structure. + - When 'io.WantCaptureMouse' or 'io.WantCaptureKeyboard' flags are set you may want to discard/hide the inputs from the rest of your application. + - When 'io.WantTextInput' is set to may want to notify your OS to popup an on-screen keyboard, if available (e.g. on a mobile phone, or console OS). + Preferably read the flags after calling ImGui::NewFrame() to avoid them lagging by one frame. But reading those flags before calling NewFrame() is + also generally ok, as the bool toggles fairly rarely and you don't generally expect to interact with either Dear ImGui or your application during + the same frame when that transition occurs. Dear ImGui is tracking dragging and widget activity that may occur outside the boundary of a window, + so 'io.WantCaptureMouse' is more accurate and correct than checking if a window is hovered. + (Advanced note: text input releases focus on Return 'KeyDown', so the following Return 'KeyUp' event that your application receive will typically + have 'io.WantCaptureKeyboard=false'. Depending on your application logic it may or not be inconvenient. You might want to track which key-downs + were for Dear ImGui, e.g. with an array of bool, and filter out the corresponding key-ups.) + + Q: How can I load a different font than the default? (default is an embedded version of ProggyClean.ttf, rendered at size 13) + A: Use the font atlas to load the TTF/OTF file you want: + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); + io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() + + New programmers: remember that in C/C++ and most programming languages if you want to use a backslash \ in a string literal you need to write a double backslash "\\": + io.Fonts->AddFontFromFileTTF("MyDataFolder\MyFontFile.ttf", size_in_pixels); // WRONG + io.Fonts->AddFontFromFileTTF("MyDataFolder\\MyFontFile.ttf", size_in_pixels); // CORRECT + io.Fonts->AddFontFromFileTTF("MyDataFolder/MyFontFile.ttf", size_in_pixels); // ALSO CORRECT + + Q: How can I easily use icons in my application? + A: The most convenient and practical way is to merge an icon font such as FontAwesome inside you main font. Then you can refer to icons within your + strings. Read 'How can I load multiple fonts?' and the file 'misc/fonts/README.txt' for instructions and useful header files. + + Q: How can I load multiple fonts? + A: Use the font atlas to pack them into a single texture: + (Read misc/fonts/README.txt and the code in ImFontAtlas for more details.) + + ImGuiIO& io = ImGui::GetIO(); + ImFont* font0 = io.Fonts->AddFontDefault(); + ImFont* font1 = io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); + ImFont* font2 = io.Fonts->AddFontFromFileTTF("myfontfile2.ttf", size_in_pixels); + io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() + // the first loaded font gets used by default + // use ImGui::PushFont()/ImGui::PopFont() to change the font at runtime + + // Options + ImFontConfig config; + config.OversampleH = 3; + config.OversampleV = 1; + config.GlyphOffset.y -= 2.0f; // Move everything by 2 pixels up + config.GlyphExtraSpacing.x = 1.0f; // Increase spacing between characters + io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, &config); + + // Combine multiple fonts into one (e.g. for icon fonts) + ImWchar ranges[] = { 0xf000, 0xf3ff, 0 }; + ImFontConfig config; + config.MergeMode = true; + io.Fonts->AddFontDefault(); + io.Fonts->LoadFromFileTTF("fontawesome-webfont.ttf", 16.0f, &config, ranges); // Merge icon font + io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, NULL, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge japanese glyphs + + Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? + A: When loading a font, pass custom Unicode ranges to specify the glyphs to load. + + // Add default Japanese ranges + io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, io.Fonts->GetGlyphRangesJapanese()); + + // Or create your own custom ranges (e.g. for a game you can feed your entire game script and only build the characters the game need) + ImVector ranges; + ImFontAtlas::GlyphRangesBuilder builder; + builder.AddText("Hello world"); // Add a string (here "Hello world" contains 7 unique characters) + builder.AddChar(0x7262); // Add a specific character + builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Add one of the default ranges + builder.BuildRanges(&ranges); // Build the final result (ordered ranges with all the unique characters submitted) + io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, ranges.Data); + + All your strings needs to use UTF-8 encoding. In C++11 you can encode a string literal in UTF-8 by using the u8"hello" syntax. + Specifying literal in your source code using a local code page (such as CP-923 for Japanese or CP-1251 for Cyrillic) will NOT work! + Otherwise you can convert yourself to UTF-8 or load text data from file already saved as UTF-8. + + Text input: it is up to your application to pass the right character code to io.AddInputCharacter(). The applications in examples/ are doing that. + For languages using IME, on Windows you can copy the Hwnd of your application to io.ImeWindowHandle. + The default implementation of io.ImeSetInputScreenPosFn() on Windows will set your IME position correctly. + + Q: How can I preserve my Dear ImGui context across reloading a DLL? (loss of the global/static variables) + A: Create your own context 'ctx = CreateContext()' + 'SetCurrentContext(ctx)' and your own font atlas 'ctx->GetIO().Fonts = new ImFontAtlas()' + so you don't rely on the default globals. + + Q: How can I use the drawing facilities without an ImGui window? (using ImDrawList API) + A: - You can create a dummy window. Call Begin() with NoTitleBar|NoResize|NoMove|NoScrollbar|NoSavedSettings|NoInputs flag, + push a ImGuiCol_WindowBg with zero alpha, then retrieve the ImDrawList* via GetWindowDrawList() and draw to it in any way you like. + - You can call ImGui::GetOverlayDrawList() and use this draw list to display contents over every other imgui windows. + - You can create your own ImDrawList instance. You'll need to initialize them ImGui::GetDrawListSharedData(), or create your own ImDrawListSharedData. + + Q: I integrated Dear ImGui in my engine and the text or lines are blurry.. + A: In your Render function, try translating your projection matrix by (0.5f,0.5f) or (0.375f,0.375f). + Also make sure your orthographic projection matrix and io.DisplaySize matches your actual framebuffer dimension. + + Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. + A: You are probably mishandling the clipping rectangles in your render function. + Rectangles provided by ImGui are defined as (x1=left,y1=top,x2=right,y2=bottom) and NOT as (x1,y1,width,height). + + + - tip: you can call Begin() multiple times with the same name during the same frame, it will keep appending to the same window. + this is also useful to set yourself in the context of another window (to get/set other settings) + - tip: you can create widgets without a Begin()/End() block, they will go in an implicit window called "Debug". + - tip: the ImGuiOnceUponAFrame helper will allow run the block of code only once a frame. You can use it to quickly add custom UI in the middle + of a deep nested inner loop in your code. + - tip: you can call Render() multiple times (e.g for VR renders). + - tip: call and read the ShowDemoWindow() code in imgui_demo.cpp for more example of how to use ImGui! + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_internal.h" + +#include // toupper, isprint +#include // NULL, malloc, free, qsort, atoi +#include // vsnprintf, sscanf, printf +#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier +#include // intptr_t +#else +#include // intptr_t +#endif + +#define IMGUI_DEBUG_NAV_SCORING 0 +#define IMGUI_DEBUG_NAV_RECTS 0 + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#endif + +// Clang warnings with -Weverything +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning : unknown warning group '-Wformat-pedantic *' // not all warnings are known by all clang versions.. so ignoring warnings triggers new warnings on some configuration. great! +#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning : format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning : declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference it. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // +#pragma clang diagnostic ignored "-Wformat-pedantic" // warning : format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning : cast to 'void *' from smaller integer type 'int' // +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'void*', but argument 6 has type 'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'xxxx' to type 'xxxx' casts away qualifiers +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#endif + +// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif + +//------------------------------------------------------------------------- +// Forward Declarations +//------------------------------------------------------------------------- + +static bool IsKeyPressedMap(ImGuiKey key, bool repeat = true); + +static ImFont* GetDefaultFont(); +static void SetCurrentWindow(ImGuiWindow* window); +static void SetWindowScrollX(ImGuiWindow* window, float new_scroll_x); +static void SetWindowScrollY(ImGuiWindow* window, float new_scroll_y); +static void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond); +static void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond); +static void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond); +static ImGuiWindow* FindHoveredWindow(); +static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFlags flags); +static void CheckStacksSize(ImGuiWindow* window, bool write); +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); + +static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list); +static void AddWindowToDrawData(ImVector* out_list, ImGuiWindow* window); +static void AddWindowToSortedBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); + +static ImGuiWindowSettings* AddWindowSettings(const char* name); + +static void LoadIniSettingsFromDisk(const char* ini_filename); +static void LoadIniSettingsFromMemory(const char* buf); +static void SaveIniSettingsToDisk(const char* ini_filename); +static void SaveIniSettingsToMemory(ImVector& out_buf); +static void MarkIniSettingsDirty(ImGuiWindow* window); + +static ImRect GetViewportRect(); + +static void ClosePopupToLevel(int remaining); +static ImGuiWindow* GetFrontMostModalRootWindow(); + +static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data); +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); +static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); + +static inline void DataTypeFormatString(ImGuiDataType data_type, void* data_ptr, const char* display_format, char* buf, int buf_size); +static inline void DataTypeFormatString(ImGuiDataType data_type, void* data_ptr, int decimal_precision, char* buf, int buf_size); +static void DataTypeApplyOp(ImGuiDataType data_type, int op, void* value1, const void* value2); +static bool DataTypeApplyOpFromText(const char* buf, const char* initial_value_buf, ImGuiDataType data_type, void* data_ptr, const char* scalar_format); + +namespace ImGui +{ +static void NavUpdate(); +static void NavUpdateWindowing(); +static void NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, const ImGuiID id); + +static void UpdateMovingWindow(); +static void UpdateManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4]); +static void FocusFrontMostActiveWindow(ImGuiWindow* ignore_window); +} + +//----------------------------------------------------------------------------- +// Platform dependent default implementations +//----------------------------------------------------------------------------- + +static const char* GetClipboardTextFn_DefaultImpl(void* user_data); +static void SetClipboardTextFn_DefaultImpl(void* user_data, const char* text); +static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y); + +//----------------------------------------------------------------------------- +// Context +//----------------------------------------------------------------------------- + +// Current context pointer. Implicitely used by all ImGui functions. Always assumed to be != NULL. +// CreateContext() will automatically set this pointer if it is NULL. Change to a different context by calling ImGui::SetCurrentContext(). +// If you use DLL hotreloading you might need to call SetCurrentContext() after reloading code from this file. +// ImGui functions are not thread-safe because of this pointer. If you want thread-safety to allow N threads to access N different contexts, you can: +// - Change this variable to use thread local storage. You may #define GImGui in imconfig.h for that purpose. Future development aim to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 +// - Having multiple instances of the ImGui code compiled inside different namespace (easiest/safest, if you have a finite number of contexts) +#ifndef GImGui +ImGuiContext* GImGui = NULL; +#endif + +// Memory Allocator functions. Use SetAllocatorFunctions() to change them. +// If you use DLL hotreloading you might need to call SetAllocatorFunctions() after reloading code from this file. +// Otherwise, you probably don't want to modify them mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +#ifndef IMGUI_DISABLE_DEFAULT_ALLOCATORS +static void* MallocWrapper(size_t size, void* user_data) { (void)user_data; return malloc(size); } +static void FreeWrapper(void* ptr, void* user_data) { (void)user_data; free(ptr); } +#else +static void* MallocWrapper(size_t size, void* user_data) { (void)user_data; (void)size; IM_ASSERT(0); return NULL; } +static void FreeWrapper(void* ptr, void* user_data) { (void)user_data; (void)ptr; IM_ASSERT(0); } +#endif + +static void* (*GImAllocatorAllocFunc)(size_t size, void* user_data) = MallocWrapper; +static void (*GImAllocatorFreeFunc)(void* ptr, void* user_data) = FreeWrapper; +static void* GImAllocatorUserData = NULL; +static size_t GImAllocatorActiveAllocationsCount = 0; + +//----------------------------------------------------------------------------- +// User facing structures +//----------------------------------------------------------------------------- + +ImGuiStyle::ImGuiStyle() +{ + Alpha = 1.0f; // Global alpha applies to everything in ImGui + WindowPadding = ImVec2(8,8); // Padding within a window + WindowRounding = 7.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows + WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. + WindowMinSize = ImVec2(32,32); // Minimum window size + WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text + ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows + ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. + PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows + PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. + FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) + FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). + FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. + ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines + ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns + ScrollbarSize = 16.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar + ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar + GrabMinSize = 10.0f; // Minimum width/height of a grab box for slider/scrollbar + GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + DisplayWindowPadding = ImVec2(22,22); // Window positions are clamped to be visible within the display area by at least this amount. Only covers regular windows. + DisplaySafeAreaPadding = ImVec2(4,4); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + AntiAliasedLines = true; // Enable anti-aliasing on lines/borders. Disable if you are really short on CPU/GPU. + AntiAliasedFill = true; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) + CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + + ImGui::StyleColorsClassic(this); +} + +// To scale your entire UI (e.g. if you want your app to use High DPI or generally be DPI aware) you may use this helper function. Scaling the fonts is done separately and is up to you. +// Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. +void ImGuiStyle::ScaleAllSizes(float scale_factor) +{ + WindowPadding = ImFloor(WindowPadding * scale_factor); + WindowRounding = ImFloor(WindowRounding * scale_factor); + WindowMinSize = ImFloor(WindowMinSize * scale_factor); + ChildRounding = ImFloor(ChildRounding * scale_factor); + PopupRounding = ImFloor(PopupRounding * scale_factor); + FramePadding = ImFloor(FramePadding * scale_factor); + FrameRounding = ImFloor(FrameRounding * scale_factor); + ItemSpacing = ImFloor(ItemSpacing * scale_factor); + ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); + TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); + IndentSpacing = ImFloor(IndentSpacing * scale_factor); + ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); + ScrollbarSize = ImFloor(ScrollbarSize * scale_factor); + ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor); + GrabMinSize = ImFloor(GrabMinSize * scale_factor); + GrabRounding = ImFloor(GrabRounding * scale_factor); + DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); + DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); + MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); +} + +ImGuiIO::ImGuiIO() +{ + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + + // Settings + DisplaySize = ImVec2(-1.0f, -1.0f); + DeltaTime = 1.0f/60.0f; + NavFlags = 0x00; + IniSavingRate = 5.0f; + IniFilename = "imgui.ini"; + LogFilename = "imgui_log.txt"; + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; + for (int i = 0; i < ImGuiKey_COUNT; i++) + KeyMap[i] = -1; + KeyRepeatDelay = 0.250f; + KeyRepeatRate = 0.050f; + UserData = NULL; + + Fonts = NULL; + FontGlobalScale = 1.0f; + FontDefault = NULL; + FontAllowUserScaling = false; + DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + DisplayVisibleMin = DisplayVisibleMax = ImVec2(0.0f, 0.0f); + + // Advanced/subtle behaviors +#ifdef __APPLE__ + OptMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag +#else + OptMacOSXBehaviors = false; +#endif + OptCursorBlink = true; + + // Settings (User Functions) + GetClipboardTextFn = GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations + SetClipboardTextFn = SetClipboardTextFn_DefaultImpl; + ClipboardUserData = NULL; + ImeSetInputScreenPosFn = ImeSetInputScreenPosFn_DefaultImpl; + ImeWindowHandle = NULL; + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + RenderDrawListsFn = NULL; +#endif + + // Input (NB: we already have memset zero the entire structure) + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); + MouseDragThreshold = 6.0f; + for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; + for (int i = 0; i < IM_ARRAYSIZE(KeysDownDuration); i++) KeysDownDuration[i] = KeysDownDurationPrev[i] = -1.0f; + for (int i = 0; i < IM_ARRAYSIZE(NavInputsDownDuration); i++) NavInputsDownDuration[i] = -1.0f; +} + +// Pass in translated ASCII characters for text input. +// - with glfw you can get those from the callback set in glfwSetCharCallback() +// - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message +void ImGuiIO::AddInputCharacter(ImWchar c) +{ + const int n = ImStrlenW(InputCharacters); + if (n + 1 < IM_ARRAYSIZE(InputCharacters)) + { + InputCharacters[n] = c; + InputCharacters[n+1] = '\0'; + } +} + +void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) +{ + // We can't pass more wchars than ImGuiIO::InputCharacters[] can hold so don't convert more + const int wchars_buf_len = sizeof(ImGuiIO::InputCharacters) / sizeof(ImWchar); + ImWchar wchars[wchars_buf_len]; + ImTextStrFromUtf8(wchars, wchars_buf_len, utf8_chars, NULL); + for (int i = 0; i < wchars_buf_len && wchars[i] != 0; i++) + AddInputCharacter(wchars[i]); +} + +//----------------------------------------------------------------------------- +// HELPERS +//----------------------------------------------------------------------------- + +#define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose +#define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 + +// Play it nice with Windows users. Notepad in 2015 still doesn't display text data with Unix-style \n. +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" +#else +#define IM_NEWLINE "\n" +#endif + +ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) +{ + ImVec2 ap = p - a; + ImVec2 ab_dir = b - a; + float dot = ap.x * ab_dir.x + ap.y * ab_dir.y; + if (dot < 0.0f) + return a; + float ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; + if (dot > ab_len_sqr) + return b; + return a + ab_dir * dot / ab_len_sqr; +} + +bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + bool b1 = ((p.x - b.x) * (a.y - b.y) - (p.y - b.y) * (a.x - b.x)) < 0.0f; + bool b2 = ((p.x - c.x) * (b.y - c.y) - (p.y - c.y) * (b.x - c.x)) < 0.0f; + bool b3 = ((p.x - a.x) * (c.y - a.y) - (p.y - a.y) * (c.x - a.x)) < 0.0f; + return ((b1 == b2) && (b2 == b3)); +} + +void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w) +{ + ImVec2 v0 = b - a; + ImVec2 v1 = c - a; + ImVec2 v2 = p - a; + const float denom = v0.x * v1.y - v1.x * v0.y; + out_v = (v2.x * v1.y - v1.x * v2.y) / denom; + out_w = (v0.x * v2.y - v2.x * v0.y) / denom; + out_u = 1.0f - out_v - out_w; +} + +ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + ImVec2 proj_ab = ImLineClosestPoint(a, b, p); + ImVec2 proj_bc = ImLineClosestPoint(b, c, p); + ImVec2 proj_ca = ImLineClosestPoint(c, a, p); + float dist2_ab = ImLengthSqr(p - proj_ab); + float dist2_bc = ImLengthSqr(p - proj_bc); + float dist2_ca = ImLengthSqr(p - proj_ca); + float m = ImMin(dist2_ab, ImMin(dist2_bc, dist2_ca)); + if (m == dist2_ab) + return proj_ab; + if (m == dist2_bc) + return proj_bc; + return proj_ca; +} + +int ImStricmp(const char* str1, const char* str2) +{ + int d; + while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } + return d; +} + +int ImStrnicmp(const char* str1, const char* str2, size_t count) +{ + int d = 0; + while (count > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; count--; } + return d; +} + +void ImStrncpy(char* dst, const char* src, size_t count) +{ + if (count < 1) return; + strncpy(dst, src, count); + dst[count-1] = 0; +} + +char* ImStrdup(const char *str) +{ + size_t len = strlen(str) + 1; + void* buf = ImGui::MemAlloc(len); + return (char*)memcpy(buf, (const void*)str, len); +} + +char* ImStrchrRange(const char* str, const char* str_end, char c) +{ + for ( ; str < str_end; str++) + if (*str == c) + return (char*)str; + return NULL; +} + +int ImStrlenW(const ImWchar* str) +{ + int n = 0; + while (*str++) n++; + return n; +} + +const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) // find beginning-of-line +{ + while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') + buf_mid_line--; + return buf_mid_line; +} + +const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end) +{ + if (!needle_end) + needle_end = needle + strlen(needle); + + const char un0 = (char)toupper(*needle); + while ((!haystack_end && *haystack) || (haystack_end && haystack < haystack_end)) + { + if (toupper(*haystack) == un0) + { + const char* b = needle + 1; + for (const char* a = haystack + 1; b < needle_end; a++, b++) + if (toupper(*a) != toupper(*b)) + break; + if (b == needle_end) + return haystack; + } + haystack++; + } + return NULL; +} + +static const char* ImAtoi(const char* src, int* output) +{ + int negative = 0; + if (*src == '-') { negative = 1; src++; } + if (*src == '+') { src++; } + int v = 0; + while (*src >= '0' && *src <= '9') + v = (v * 10) + (*src++ - '0'); + *output = negative ? -v : v; + return src; +} + +// A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). +// Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. +// B) When buf==NULL vsnprintf() will return the output size. +#ifndef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS +int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int w = vsnprintf(buf, buf_size, fmt, args); + va_end(args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} + +int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) +{ + int w = vsnprintf(buf, buf_size, fmt, args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} +#endif // #ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS + +// Pass data_size==0 for zero-terminated strings +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImU32 ImHash(const void* data, int data_size, ImU32 seed) +{ + static ImU32 crc32_lut[256] = { 0 }; + if (!crc32_lut[1]) + { + const ImU32 polynomial = 0xEDB88320; + for (ImU32 i = 0; i < 256; i++) + { + ImU32 crc = i; + for (ImU32 j = 0; j < 8; j++) + crc = (crc >> 1) ^ (ImU32(-int(crc & 1)) & polynomial); + crc32_lut[i] = crc; + } + } + + seed = ~seed; + ImU32 crc = seed; + const unsigned char* current = (const unsigned char*)data; + + if (data_size > 0) + { + // Known size + while (data_size--) + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *current++]; + } + else + { + // Zero-terminated string + while (unsigned char c = *current++) + { + // We support a syntax of "label###id" where only "###id" is included in the hash, and only "label" gets displayed. + // Because this syntax is rarely used we are optimizing for the common case. + // - If we reach ### in the string we discard the hash so far and reset to the seed. + // - We don't do 'current += 2; continue;' after handling ### to keep the code smaller. + if (c == '#' && current[0] == '#' && current[1] == '#') + crc = seed; + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + } + } + return ~crc; +} + +//----------------------------------------------------------------------------- +// ImText* helpers +//----------------------------------------------------------------------------- + +// Convert UTF-8 to 32-bits character, process single character input. +// Based on stb_from_utf8() from github.com/nothings/stb/ +// We handle UTF-8 decoding error by skipping forward. +int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end) +{ + unsigned int c = (unsigned int)-1; + const unsigned char* str = (const unsigned char*)in_text; + if (!(*str & 0x80)) + { + c = (unsigned int)(*str++); + *out_char = c; + return 1; + } + if ((*str & 0xe0) == 0xc0) + { + *out_char = 0xFFFD; // will be invalid but not end of string + if (in_text_end && in_text_end - (const char*)str < 2) return 1; + if (*str < 0xc2) return 2; + c = (unsigned int)((*str++ & 0x1f) << 6); + if ((*str & 0xc0) != 0x80) return 2; + c += (*str++ & 0x3f); + *out_char = c; + return 2; + } + if ((*str & 0xf0) == 0xe0) + { + *out_char = 0xFFFD; // will be invalid but not end of string + if (in_text_end && in_text_end - (const char*)str < 3) return 1; + if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) return 3; + if (*str == 0xed && str[1] > 0x9f) return 3; // str[1] < 0x80 is checked below + c = (unsigned int)((*str++ & 0x0f) << 12); + if ((*str & 0xc0) != 0x80) return 3; + c += (unsigned int)((*str++ & 0x3f) << 6); + if ((*str & 0xc0) != 0x80) return 3; + c += (*str++ & 0x3f); + *out_char = c; + return 3; + } + if ((*str & 0xf8) == 0xf0) + { + *out_char = 0xFFFD; // will be invalid but not end of string + if (in_text_end && in_text_end - (const char*)str < 4) return 1; + if (*str > 0xf4) return 4; + if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) return 4; + if (*str == 0xf4 && str[1] > 0x8f) return 4; // str[1] < 0x80 is checked below + c = (unsigned int)((*str++ & 0x07) << 18); + if ((*str & 0xc0) != 0x80) return 4; + c += (unsigned int)((*str++ & 0x3f) << 12); + if ((*str & 0xc0) != 0x80) return 4; + c += (unsigned int)((*str++ & 0x3f) << 6); + if ((*str & 0xc0) != 0x80) return 4; + c += (*str++ & 0x3f); + // utf-8 encodings of values used in surrogate pairs are invalid + if ((c & 0xFFFFF800) == 0xD800) return 4; + *out_char = c; + return 4; + } + *out_char = 0; + return 0; +} + +int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_text_remaining) +{ + ImWchar* buf_out = buf; + ImWchar* buf_end = buf + buf_size; + while (buf_out < buf_end-1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + if (c == 0) + break; + if (c < 0x10000) // FIXME: Losing characters that don't fit in 2 bytes + *buf_out++ = (ImWchar)c; + } + *buf_out = 0; + if (in_text_remaining) + *in_text_remaining = in_text; + return (int)(buf_out - buf); +} + +int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end) +{ + int char_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + if (c == 0) + break; + if (c < 0x10000) + char_count++; + } + return char_count; +} + +// Based on stb_to_utf8() from github.com/nothings/stb/ +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) + { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c ) & 0x3f)); + return 4; + } + //else if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c>> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c ) & 0x3f)); + return 3; + } +} + +static inline int ImTextCountUtf8BytesFromChar(unsigned int c) +{ + if (c < 0x80) return 1; + if (c < 0x800) return 2; + if (c >= 0xdc00 && c < 0xe000) return 0; + if (c >= 0xd800 && c < 0xdc00) return 4; + return 3; +} + +int ImTextStrToUtf8(char* buf, int buf_size, const ImWchar* in_text, const ImWchar* in_text_end) +{ + char* buf_out = buf; + const char* buf_end = buf + buf_size; + while (buf_out < buf_end-1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + *buf_out++ = (char)c; + else + buf_out += ImTextCharToUtf8(buf_out, (int)(buf_end-buf_out-1), c); + } + *buf_out = 0; + return (int)(buf_out - buf); +} + +int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end) +{ + int bytes_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + bytes_count++; + else + bytes_count += ImTextCountUtf8BytesFromChar(c); + } + return bytes_count; +} + +ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) +{ + float s = 1.0f/255.0f; + return ImVec4( + ((in >> IM_COL32_R_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s); +} + +ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4& in) +{ + ImU32 out; + out = ((ImU32)IM_F32_TO_INT8_SAT(in.x)) << IM_COL32_R_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.y)) << IM_COL32_G_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.z)) << IM_COL32_B_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.w)) << IM_COL32_A_SHIFT; + return out; +} + +ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = style.Colors[idx]; + c.w *= style.Alpha * alpha_mul; + return ColorConvertFloat4ToU32(c); +} + +ImU32 ImGui::GetColorU32(const ImVec4& col) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = col; + c.w *= style.Alpha; + return ColorConvertFloat4ToU32(c); +} + +const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) +{ + ImGuiStyle& style = GImGui->Style; + return style.Colors[idx]; +} + +ImU32 ImGui::GetColorU32(ImU32 col) +{ + float style_alpha = GImGui->Style.Alpha; + if (style_alpha >= 1.0f) + return col; + int a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; + a = (int)(a * style_alpha); // We don't need to clamp 0..255 because Style.Alpha is in 0..1 range. + return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); +} + +// Convert rgb floats ([0-1],[0-1],[0-1]) to hsv floats ([0-1],[0-1],[0-1]), from Foley & van Dam p592 +// Optimized http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv +void ImGui::ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v) +{ + float K = 0.f; + if (g < b) + { + ImSwap(g, b); + K = -1.f; + } + if (r < g) + { + ImSwap(r, g); + K = -2.f / 6.f - K; + } + + const float chroma = r - (g < b ? g : b); + out_h = fabsf(K + (g - b) / (6.f * chroma + 1e-20f)); + out_s = chroma / (r + 1e-20f); + out_v = r; +} + +// Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593 +// also http://en.wikipedia.org/wiki/HSL_and_HSV +void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b) +{ + if (s == 0.0f) + { + // gray + out_r = out_g = out_b = v; + return; + } + + h = fmodf(h, 1.0f) / (60.0f/360.0f); + int i = (int)h; + float f = h - (float)i; + float p = v * (1.0f - s); + float q = v * (1.0f - s * f); + float t = v * (1.0f - s * (1.0f - f)); + + switch (i) + { + case 0: out_r = v; out_g = t; out_b = p; break; + case 1: out_r = q; out_g = v; out_b = p; break; + case 2: out_r = p; out_g = v; out_b = t; break; + case 3: out_r = p; out_g = q; out_b = v; break; + case 4: out_r = t; out_g = p; out_b = v; break; + case 5: default: out_r = v; out_g = p; out_b = q; break; + } +} + +FILE* ImFileOpen(const char* filename, const char* mode) +{ +#if defined(_WIN32) && !defined(__CYGWIN__) + // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. Converting both strings from UTF-8 to wchar format (using a single allocation, because we can) + const int filename_wsize = ImTextCountCharsFromUtf8(filename, NULL) + 1; + const int mode_wsize = ImTextCountCharsFromUtf8(mode, NULL) + 1; + ImVector buf; + buf.resize(filename_wsize + mode_wsize); + ImTextStrFromUtf8(&buf[0], filename_wsize, filename, NULL); + ImTextStrFromUtf8(&buf[filename_wsize], mode_wsize, mode, NULL); + return _wfopen((wchar_t*)&buf[0], (wchar_t*)&buf[filename_wsize]); +#else + return fopen(filename, mode); +#endif +} + +// Load file content into memory +// Memory allocated with ImGui::MemAlloc(), must be freed by user using ImGui::MemFree() +void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, int* out_file_size, int padding_bytes) +{ + IM_ASSERT(filename && file_open_mode); + if (out_file_size) + *out_file_size = 0; + + FILE* f; + if ((f = ImFileOpen(filename, file_open_mode)) == NULL) + return NULL; + + long file_size_signed; + if (fseek(f, 0, SEEK_END) || (file_size_signed = ftell(f)) == -1 || fseek(f, 0, SEEK_SET)) + { + fclose(f); + return NULL; + } + + int file_size = (int)file_size_signed; + void* file_data = ImGui::MemAlloc(file_size + padding_bytes); + if (file_data == NULL) + { + fclose(f); + return NULL; + } + if (fread(file_data, 1, (size_t)file_size, f) != (size_t)file_size) + { + fclose(f); + ImGui::MemFree(file_data); + return NULL; + } + if (padding_bytes > 0) + memset((void *)(((char*)file_data) + file_size), 0, padding_bytes); + + fclose(f); + if (out_file_size) + *out_file_size = file_size; + + return file_data; +} + +//----------------------------------------------------------------------------- +// ImGuiStorage +// Helper: Key->value storage +//----------------------------------------------------------------------------- + +// std::lower_bound but without the bullshit +static ImVector::iterator LowerBound(ImVector& data, ImGuiID key) +{ + ImVector::iterator first = data.begin(); + ImVector::iterator last = data.end(); + size_t count = (size_t)(last - first); + while (count > 0) + { + size_t count2 = count >> 1; + ImVector::iterator mid = first + count2; + if (mid->key < key) + { + first = ++mid; + count -= count2 + 1; + } + else + { + count = count2; + } + } + return first; +} + +// For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. +void ImGuiStorage::BuildSortByKey() +{ + struct StaticFunc + { + static int IMGUI_CDECL PairCompareByID(const void* lhs, const void* rhs) + { + // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. + if (((const Pair*)lhs)->key > ((const Pair*)rhs)->key) return +1; + if (((const Pair*)lhs)->key < ((const Pair*)rhs)->key) return -1; + return 0; + } + }; + if (Data.Size > 1) + qsort(Data.Data, (size_t)Data.Size, sizeof(Pair), StaticFunc::PairCompareByID); +} + +int ImGuiStorage::GetInt(ImGuiID key, int default_val) const +{ + ImVector::iterator it = LowerBound(const_cast&>(Data), key); + if (it == Data.end() || it->key != key) + return default_val; + return it->val_i; +} + +bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const +{ + return GetInt(key, default_val ? 1 : 0) != 0; +} + +float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const +{ + ImVector::iterator it = LowerBound(const_cast&>(Data), key); + if (it == Data.end() || it->key != key) + return default_val; + return it->val_f; +} + +void* ImGuiStorage::GetVoidPtr(ImGuiID key) const +{ + ImVector::iterator it = LowerBound(const_cast&>(Data), key); + if (it == Data.end() || it->key != key) + return NULL; + return it->val_p; +} + +// References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. +int* ImGuiStorage::GetIntRef(ImGuiID key, int default_val) +{ + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + it = Data.insert(it, Pair(key, default_val)); + return &it->val_i; +} + +bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) +{ + return (bool*)GetIntRef(key, default_val ? 1 : 0); +} + +float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) +{ + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + it = Data.insert(it, Pair(key, default_val)); + return &it->val_f; +} + +void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) +{ + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + it = Data.insert(it, Pair(key, default_val)); + return &it->val_p; +} + +// FIXME-OPT: Need a way to reuse the result of lower_bound when doing GetInt()/SetInt() - not too bad because it only happens on explicit interaction (maximum one a frame) +void ImGuiStorage::SetInt(ImGuiID key, int val) +{ + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, Pair(key, val)); + return; + } + it->val_i = val; +} + +void ImGuiStorage::SetBool(ImGuiID key, bool val) +{ + SetInt(key, val ? 1 : 0); +} + +void ImGuiStorage::SetFloat(ImGuiID key, float val) +{ + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, Pair(key, val)); + return; + } + it->val_f = val; +} + +void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) +{ + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, Pair(key, val)); + return; + } + it->val_p = val; +} + +void ImGuiStorage::SetAllInt(int v) +{ + for (int i = 0; i < Data.Size; i++) + Data[i].val_i = v; +} + +//----------------------------------------------------------------------------- +// ImGuiTextFilter +//----------------------------------------------------------------------------- + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) +{ + if (default_filter) + { + ImStrncpy(InputBuf, default_filter, IM_ARRAYSIZE(InputBuf)); + Build(); + } + else + { + InputBuf[0] = 0; + CountGrep = 0; + } +} + +bool ImGuiTextFilter::Draw(const char* label, float width) +{ + if (width != 0.0f) + ImGui::PushItemWidth(width); + bool value_changed = ImGui::InputText(label, InputBuf, IM_ARRAYSIZE(InputBuf)); + if (width != 0.0f) + ImGui::PopItemWidth(); + if (value_changed) + Build(); + return value_changed; +} + +void ImGuiTextFilter::TextRange::split(char separator, ImVector& out) +{ + out.resize(0); + const char* wb = b; + const char* we = wb; + while (we < e) + { + if (*we == separator) + { + out.push_back(TextRange(wb, we)); + wb = we + 1; + } + we++; + } + if (wb != we) + out.push_back(TextRange(wb, we)); +} + +void ImGuiTextFilter::Build() +{ + Filters.resize(0); + TextRange input_range(InputBuf, InputBuf+strlen(InputBuf)); + input_range.split(',', Filters); + + CountGrep = 0; + for (int i = 0; i != Filters.Size; i++) + { + Filters[i].trim_blanks(); + if (Filters[i].empty()) + continue; + if (Filters[i].front() != '-') + CountGrep += 1; + } +} + +bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const +{ + if (Filters.empty()) + return true; + + if (text == NULL) + text = ""; + + for (int i = 0; i != Filters.Size; i++) + { + const TextRange& f = Filters[i]; + if (f.empty()) + continue; + if (f.front() == '-') + { + // Subtract + if (ImStristr(text, text_end, f.begin()+1, f.end()) != NULL) + return false; + } + else + { + // Grep + if (ImStristr(text, text_end, f.begin(), f.end()) != NULL) + return true; + } + } + + // Implicit * grep + if (CountGrep == 0) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// ImGuiTextBuffer +//----------------------------------------------------------------------------- + +// On some platform vsnprintf() takes va_list by reference and modifies it. +// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. +#ifndef va_copy +#define va_copy(dest, src) (dest = src) +#endif + +// Helper: Text buffer for logging/accumulating text +void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) +{ + va_list args_copy; + va_copy(args_copy, args); + + int len = ImFormatStringV(NULL, 0, fmt, args); // FIXME-OPT: could do a first pass write attempt, likely successful on first pass. + if (len <= 0) + return; + + const int write_off = Buf.Size; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int double_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > double_capacity ? needed_sz : double_capacity); + } + + Buf.resize(needed_sz); + ImFormatStringV(&Buf[write_off - 1], len + 1, fmt, args_copy); +} + +void ImGuiTextBuffer::appendf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + appendfv(fmt, args); + va_end(args); +} + +//----------------------------------------------------------------------------- +// ImGuiSimpleColumns (internal use only) +//----------------------------------------------------------------------------- + +ImGuiMenuColumns::ImGuiMenuColumns() +{ + Count = 0; + Spacing = Width = NextWidth = 0.0f; + memset(Pos, 0, sizeof(Pos)); + memset(NextWidths, 0, sizeof(NextWidths)); +} + +void ImGuiMenuColumns::Update(int count, float spacing, bool clear) +{ + IM_ASSERT(Count <= IM_ARRAYSIZE(Pos)); + Count = count; + Width = NextWidth = 0.0f; + Spacing = spacing; + if (clear) memset(NextWidths, 0, sizeof(NextWidths)); + for (int i = 0; i < Count; i++) + { + if (i > 0 && NextWidths[i] > 0.0f) + Width += Spacing; + Pos[i] = (float)(int)Width; + Width += NextWidths[i]; + NextWidths[i] = 0.0f; + } +} + +float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double +{ + NextWidth = 0.0f; + NextWidths[0] = ImMax(NextWidths[0], w0); + NextWidths[1] = ImMax(NextWidths[1], w1); + NextWidths[2] = ImMax(NextWidths[2], w2); + for (int i = 0; i < 3; i++) + NextWidth += NextWidths[i] + ((i > 0 && NextWidths[i] > 0.0f) ? Spacing : 0.0f); + return ImMax(Width, NextWidth); +} + +float ImGuiMenuColumns::CalcExtraSpace(float avail_w) +{ + return ImMax(0.0f, avail_w - Width); +} + +//----------------------------------------------------------------------------- +// ImGuiListClipper +//----------------------------------------------------------------------------- + +static void SetCursorPosYAndSetupDummyPrevLine(float pos_y, float line_height) +{ + // Set cursor position and a few other things so that SetScrollHere() and Columns() can work when seeking cursor. + // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. Consider moving within SetCursorXXX functions? + ImGui::SetCursorPosY(pos_y); + ImGuiWindow* window = ImGui::GetCurrentWindow(); + window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHere() can properly function after the end of our clipper usage. + window->DC.PrevLineHeight = (line_height - GImGui->Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. + if (window->DC.ColumnsSet) + window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly +} + +// Use case A: Begin() called from constructor with items_height<0, then called again from Sync() in StepNo 1 +// Use case B: Begin() called from constructor with items_height>0 +// FIXME-LEGACY: Ideally we should remove the Begin/End functions but they are part of the legacy API we still support. This is why some of the code in Step() calling Begin() and reassign some fields, spaghetti style. +void ImGuiListClipper::Begin(int count, float items_height) +{ + StartPosY = ImGui::GetCursorPosY(); + ItemsHeight = items_height; + ItemsCount = count; + StepNo = 0; + DisplayEnd = DisplayStart = -1; + if (ItemsHeight > 0.0f) + { + ImGui::CalcListClipping(ItemsCount, ItemsHeight, &DisplayStart, &DisplayEnd); // calculate how many to clip/display + if (DisplayStart > 0) + SetCursorPosYAndSetupDummyPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor + StepNo = 2; + } +} + +void ImGuiListClipper::End() +{ + if (ItemsCount < 0) + return; + // In theory here we should assert that ImGui::GetCursorPosY() == StartPosY + DisplayEnd * ItemsHeight, but it feels saner to just seek at the end and not assert/crash the user. + if (ItemsCount < INT_MAX) + SetCursorPosYAndSetupDummyPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); // advance cursor + ItemsCount = -1; + StepNo = 3; +} + +bool ImGuiListClipper::Step() +{ + if (ItemsCount == 0 || ImGui::GetCurrentWindowRead()->SkipItems) + { + ItemsCount = -1; + return false; + } + if (StepNo == 0) // Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height. + { + DisplayStart = 0; + DisplayEnd = 1; + StartPosY = ImGui::GetCursorPosY(); + StepNo = 1; + return true; + } + if (StepNo == 1) // Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. + { + if (ItemsCount == 1) { ItemsCount = -1; return false; } + float items_height = ImGui::GetCursorPosY() - StartPosY; + IM_ASSERT(items_height > 0.0f); // If this triggers, it means Item 0 hasn't moved the cursor vertically + Begin(ItemsCount-1, items_height); + DisplayStart++; + DisplayEnd++; + StepNo = 3; + return true; + } + if (StepNo == 2) // Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user still call Step(). Does nothing and switch to Step 3. + { + IM_ASSERT(DisplayStart >= 0 && DisplayEnd >= 0); + StepNo = 3; + return true; + } + if (StepNo == 3) // Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. + End(); + return false; +} + +//----------------------------------------------------------------------------- +// ImGuiWindow +//----------------------------------------------------------------------------- + +ImGuiWindow::ImGuiWindow(ImGuiContext* context, const char* name) +{ + Name = ImStrdup(name); + ID = ImHash(name, 0); + IDStack.push_back(ID); + Flags = 0; + PosFloat = Pos = ImVec2(0.0f, 0.0f); + Size = SizeFull = ImVec2(0.0f, 0.0f); + SizeContents = SizeContentsExplicit = ImVec2(0.0f, 0.0f); + WindowPadding = ImVec2(0.0f, 0.0f); + WindowRounding = 0.0f; + WindowBorderSize = 0.0f; + MoveId = GetID("#MOVE"); + ChildId = 0; + Scroll = ImVec2(0.0f, 0.0f); + ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); + ScrollbarX = ScrollbarY = false; + ScrollbarSizes = ImVec2(0.0f, 0.0f); + Active = WasActive = false; + WriteAccessed = false; + Collapsed = false; + CollapseToggleWanted = false; + SkipItems = false; + Appearing = false; + CloseButton = false; + BeginOrderWithinParent = -1; + BeginOrderWithinContext = -1; + BeginCount = 0; + PopupId = 0; + AutoFitFramesX = AutoFitFramesY = -1; + AutoFitOnlyGrows = false; + AutoFitChildAxises = 0x00; + AutoPosLastDirection = ImGuiDir_None; + HiddenFrames = 0; + SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; + SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); + + LastFrameActive = -1; + ItemWidthDefault = 0.0f; + FontWindowScale = 1.0f; + + DrawList = IM_NEW(ImDrawList)(&context->DrawListSharedData); + DrawList->_OwnerName = Name; + ParentWindow = NULL; + RootWindow = NULL; + RootWindowForTitleBarHighlight = NULL; + RootWindowForTabbing = NULL; + RootWindowForNav = NULL; + + NavLastIds[0] = NavLastIds[1] = 0; + NavRectRel[0] = NavRectRel[1] = ImRect(); + NavLastChildNavWindow = NULL; + + FocusIdxAllCounter = FocusIdxTabCounter = -1; + FocusIdxAllRequestCurrent = FocusIdxTabRequestCurrent = INT_MAX; + FocusIdxAllRequestNext = FocusIdxTabRequestNext = INT_MAX; +} + +ImGuiWindow::~ImGuiWindow() +{ + IM_DELETE(DrawList); + IM_DELETE(Name); + for (int i = 0; i != ColumnsStorage.Size; i++) + ColumnsStorage[i].~ImGuiColumnsSet(); +} + +ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHash(str, str_end ? (int)(str_end - str) : 0, seed); + ImGui::KeepAliveID(id); + return id; +} + +ImGuiID ImGuiWindow::GetID(const void* ptr) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHash(&ptr, sizeof(void*), seed); + ImGui::KeepAliveID(id); + return id; +} + +ImGuiID ImGuiWindow::GetIDNoKeepAlive(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + return ImHash(str, str_end ? (int)(str_end - str) : 0, seed); +} + +// This is only used in rare/specific situations to manufacture an ID out of nowhere. +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) +{ + ImGuiID seed = IDStack.back(); + const int r_rel[4] = { (int)(r_abs.Min.x - Pos.x), (int)(r_abs.Min.y - Pos.y), (int)(r_abs.Max.x - Pos.x), (int)(r_abs.Max.y - Pos.y) }; + ImGuiID id = ImHash(&r_rel, sizeof(r_rel), seed); + ImGui::KeepAliveID(id); + return id; +} + +//----------------------------------------------------------------------------- +// Internal API exposed in imgui_internal.h +//----------------------------------------------------------------------------- + +static void SetCurrentWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow = window; + if (window) + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); +} + +static void SetNavID(ImGuiID id, int nav_layer) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow); + IM_ASSERT(nav_layer == 0 || nav_layer == 1); + g.NavId = id; + g.NavWindow->NavLastIds[nav_layer] = id; +} + +static void SetNavIDAndMoveMouse(ImGuiID id, int nav_layer, const ImRect& rect_rel) +{ + ImGuiContext& g = *GImGui; + SetNavID(id, nav_layer); + g.NavWindow->NavRectRel[nav_layer] = rect_rel; + g.NavMousePosDirty = true; + g.NavDisableHighlight = false; + g.NavDisableMouseHover = true; +} + +void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.ActiveIdIsJustActivated = (g.ActiveId != id); + if (g.ActiveIdIsJustActivated) + g.ActiveIdTimer = 0.0f; + g.ActiveId = id; + g.ActiveIdAllowNavDirFlags = 0; + g.ActiveIdAllowOverlap = false; + g.ActiveIdWindow = window; + if (id) + { + g.ActiveIdIsAlive = true; + g.ActiveIdSource = (g.NavActivateId == id || g.NavInputId == id || g.NavJustTabbedId == id || g.NavJustMovedToId == id) ? ImGuiInputSource_Nav : ImGuiInputSource_Mouse; + } +} + +ImGuiID ImGui::GetActiveID() +{ + ImGuiContext& g = *GImGui; + return g.ActiveId; +} + +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + // Assume that SetFocusID() is called in the context where its NavLayer is the current layer, which is the case everywhere we call it. + const int nav_layer = window->DC.NavLayerCurrent; + if (g.NavWindow != window) + g.NavInitRequest = false; + g.NavId = id; + g.NavWindow = window; + g.NavLayer = nav_layer; + window->NavLastIds[nav_layer] = id; + if (window->DC.LastItemId == id) + window->NavRectRel[nav_layer] = ImRect(window->DC.LastItemRect.Min - window->Pos, window->DC.LastItemRect.Max - window->Pos); + + if (g.ActiveIdSource == ImGuiInputSource_Nav) + g.NavDisableMouseHover = true; + else + g.NavDisableHighlight = true; +} + +void ImGui::ClearActiveID() +{ + SetActiveID(0, NULL); +} + +void ImGui::SetHoveredID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.HoveredId = id; + g.HoveredIdAllowOverlap = false; + g.HoveredIdTimer = (id != 0 && g.HoveredIdPreviousFrame == id) ? (g.HoveredIdTimer + g.IO.DeltaTime) : 0.0f; +} + +ImGuiID ImGui::GetHoveredID() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; +} + +void ImGui::KeepAliveID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = true; +} + +static inline bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) +{ + // An active popup disable hovering on other windows (apart from its own children) + // FIXME-OPT: This could be cached/stored within the window. + ImGuiContext& g = *GImGui; + if (g.NavWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) + if (focused_root_window->WasActive && focused_root_window != window->RootWindow) + { + // For the purpose of those flags we differentiate "standard popup" from "modal popup" + // NB: The order of those two tests is important because Modal windows are also Popups. + if (focused_root_window->Flags & ImGuiWindowFlags_Modal) + return false; + if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + return false; + } + + return true; +} + +// Advance cursor given item size for layout. +void ImGui::ItemSize(const ImVec2& size, float text_offset_y) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // Always align ourselves on pixel boundaries + const float line_height = ImMax(window->DC.CurrentLineHeight, size.y); + const float text_base_offset = ImMax(window->DC.CurrentLineTextBaseOffset, text_offset_y); + //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] + window->DC.CursorPosPrevLine = ImVec2(window->DC.CursorPos.x + size.x, window->DC.CursorPos.y); + window->DC.CursorPos = ImVec2((float)(int)(window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX), (float)(int)(window->DC.CursorPos.y + line_height + g.Style.ItemSpacing.y)); + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); + //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] + + window->DC.PrevLineHeight = line_height; + window->DC.PrevLineTextBaseOffset = text_base_offset; + window->DC.CurrentLineHeight = window->DC.CurrentLineTextBaseOffset = 0.0f; + + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); +} + +void ImGui::ItemSize(const ImRect& bb, float text_offset_y) +{ + ItemSize(bb.GetSize(), text_offset_y); +} + +static ImGuiDir NavScoreItemGetQuadrant(float dx, float dy) +{ + if (fabsf(dx) > fabsf(dy)) + return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; + return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; +} + +static float NavScoreItemDistInterval(float a0, float a1, float b0, float b1) +{ + if (a1 < b0) + return a1 - b0; + if (b1 < a0) + return a0 - b1; + return 0.0f; +} + +// Scoring function for directional navigation. Based on https://gist.github.com/rygorous/6981057 +static bool NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavLayer != window->DC.NavLayerCurrent) + return false; + + const ImRect& curr = g.NavScoringRectScreen; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) + g.NavScoringCount++; + + // We perform scoring on items bounding box clipped by their parent window on the other axis (clipping on our movement axis would give us equal scores for all clipped items) + if (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) + { + cand.Min.y = ImClamp(cand.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y); + cand.Max.y = ImClamp(cand.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y); + } + else + { + cand.Min.x = ImClamp(cand.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + cand.Max.x = ImClamp(cand.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + } + + // Compute distance between boxes + // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. + float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); + float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items + if (dby != 0.0f && dbx != 0.0f) + dbx = (dbx/1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); + float dist_box = fabsf(dbx) + fabsf(dby); + + // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) + float dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x); + float dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y); + float dist_center = fabsf(dcx) + fabsf(dcy); // L1 metric (need this for our connectedness guarantee) + + // Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance + ImGuiDir quadrant; + float dax = 0.0f, day = 0.0f, dist_axial = 0.0f; + if (dbx != 0.0f || dby != 0.0f) + { + // For non-overlapping boxes, use distance between boxes + dax = dbx; + day = dby; + dist_axial = dist_box; + quadrant = NavScoreItemGetQuadrant(dbx, dby); + } + else if (dcx != 0.0f || dcy != 0.0f) + { + // For overlapping boxes with different centers, use distance between centers + dax = dcx; + day = dcy; + dist_axial = dist_center; + quadrant = NavScoreItemGetQuadrant(dcx, dcy); + } + else + { + // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) + quadrant = (window->DC.LastItemId < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; + } + +#if IMGUI_DEBUG_NAV_SCORING + char buf[128]; + if (ImGui::IsMouseHoveringRect(cand.Min, cand.Max)) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "dbox (%.2f,%.2f->%.4f)\ndcen (%.2f,%.2f->%.4f)\nd (%.2f,%.2f->%.4f)\nnav %c, quadrant %c", dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "WENS"[g.NavMoveDir], "WENS"[quadrant]); + g.OverlayDrawList.AddRect(curr.Min, curr.Max, IM_COL32(255, 200, 0, 100)); + g.OverlayDrawList.AddRect(cand.Min, cand.Max, IM_COL32(255,255,0,200)); + g.OverlayDrawList.AddRectFilled(cand.Max-ImVec2(4,4), cand.Max+ImGui::CalcTextSize(buf)+ImVec2(4,4), IM_COL32(40,0,0,150)); + g.OverlayDrawList.AddText(g.IO.FontDefault, 13.0f, cand.Max, ~0U, buf); + } + else if (g.IO.KeyCtrl) // Hold to preview score in matching quadrant. Press C to rotate. + { + if (IsKeyPressedMap(ImGuiKey_C)) { g.NavMoveDirLast = (ImGuiDir)((g.NavMoveDirLast + 1) & 3); g.IO.KeysDownDuration[g.IO.KeyMap[ImGuiKey_C]] = 0.01f; } + if (quadrant == g.NavMoveDir) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f/%.0f", dist_box, dist_center); + g.OverlayDrawList.AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 200)); + g.OverlayDrawList.AddText(g.IO.FontDefault, 13.0f, cand.Min, IM_COL32(255, 255, 255, 255), buf); + } + } + #endif + + // Is it in the quadrant we're interesting in moving to? + bool new_best = false; + if (quadrant == g.NavMoveDir) + { + // Does it beat the current best candidate? + if (dist_box < result->DistBox) + { + result->DistBox = dist_box; + result->DistCenter = dist_center; + return true; + } + if (dist_box == result->DistBox) + { + // Try using distance between center points to break ties + if (dist_center < result->DistCenter) + { + result->DistCenter = dist_center; + new_best = true; + } + else if (dist_center == result->DistCenter) + { + // Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items + // (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index), + // this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis. + if (((g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance + new_best = true; + } + } + } + + // Axial check: if 'curr' has no link at all in some direction and 'cand' lies roughly in that direction, add a tentative link. This will only be kept if no "real" matches + // are found, so it only augments the graph produced by the above method using extra links. (important, since it doesn't guarantee strong connectedness) + // This is just to avoid buttons having no links in a particular direction when there's a suitable neighbor. you get good graphs without this too. + // 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward. + // Disabling it may however lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option? + if (result->DistBox == FLT_MAX && dist_axial < result->DistAxial) // Check axial match + if (g.NavLayer == 1 && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + if ((g.NavMoveDir == ImGuiDir_Left && dax < 0.0f) || (g.NavMoveDir == ImGuiDir_Right && dax > 0.0f) || (g.NavMoveDir == ImGuiDir_Up && day < 0.0f) || (g.NavMoveDir == ImGuiDir_Down && day > 0.0f)) + { + result->DistAxial = dist_axial; + new_best = true; + } + + return new_best; +} + +static void NavSaveLastChildNavWindow(ImGuiWindow* child_window) +{ + ImGuiWindow* parent_window = child_window; + while (parent_window && (parent_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (parent_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + parent_window = parent_window->ParentWindow; + if (parent_window && parent_window != child_window) + parent_window->NavLastChildNavWindow = child_window; +} + +// Call when we are expected to land on Layer 0 after FocusWindow() +static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window) +{ + return window->NavLastChildNavWindow ? window->NavLastChildNavWindow : window; +} + +static void NavRestoreLayer(int layer) +{ + ImGuiContext& g = *GImGui; + g.NavLayer = layer; + if (layer == 0) + g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); + if (layer == 0 && g.NavWindow->NavLastIds[0] != 0) + SetNavIDAndMoveMouse(g.NavWindow->NavLastIds[0], layer, g.NavWindow->NavRectRel[0]); + else + ImGui::NavInitWindow(g.NavWindow, true); +} + +static inline void NavUpdateAnyRequestFlag() +{ + ImGuiContext& g = *GImGui; + g.NavAnyRequest = g.NavMoveRequest || g.NavInitRequest || IMGUI_DEBUG_NAV_SCORING; +} + +static bool NavMoveRequestButNoResultYet() +{ + ImGuiContext& g = *GImGui; + return g.NavMoveRequest && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +} + +void ImGui::NavMoveRequestCancel() +{ + ImGuiContext& g = *GImGui; + g.NavMoveRequest = false; + NavUpdateAnyRequestFlag(); +} + +// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) +static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, const ImGuiID id) +{ + ImGuiContext& g = *GImGui; + //if (!g.IO.NavActive) // [2017/10/06] Removed this possibly redundant test but I am not sure of all the side-effects yet. Some of the feature here will need to work regardless of using a _NoNavInputs flag. + // return; + + const ImGuiItemFlags item_flags = window->DC.ItemFlags; + const ImRect nav_bb_rel(nav_bb.Min - window->Pos, nav_bb.Max - window->Pos); + if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent) + { + // Even if 'ImGuiItemFlags_NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback + if (!(item_flags & ImGuiItemFlags_NoNavDefaultFocus) || g.NavInitResultId == 0) + { + g.NavInitResultId = id; + g.NavInitResultRectRel = nav_bb_rel; + } + if (!(item_flags & ImGuiItemFlags_NoNavDefaultFocus)) + { + g.NavInitRequest = false; // Found a match, clear request + NavUpdateAnyRequestFlag(); + } + } + + // Scoring for navigation + if (g.NavId != id && !(item_flags & ImGuiItemFlags_NoNav)) + { + ImGuiNavMoveResult* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; +#if IMGUI_DEBUG_NAV_SCORING + // [DEBUG] Score all items in NavWindow at all times + if (!g.NavMoveRequest) + g.NavMoveDir = g.NavMoveDirLast; + bool new_best = NavScoreItem(result, nav_bb) && g.NavMoveRequest; +#else + bool new_best = g.NavMoveRequest && NavScoreItem(result, nav_bb); +#endif + if (new_best) + { + result->ID = id; + result->ParentID = window->IDStack.back(); + result->Window = window; + result->RectRel = nav_bb_rel; + } + } + + // Update window-relative bounding box of navigated item + if (g.NavId == id) + { + g.NavWindow = window; // Always refresh g.NavWindow, because some operations such as FocusItem() don't have a window. + g.NavLayer = window->DC.NavLayerCurrent; + g.NavIdIsAlive = true; + g.NavIdTabCounter = window->FocusIdxTabCounter; + window->NavRectRel[window->DC.NavLayerCurrent] = nav_bb_rel; // Store item bounding box (relative to window position) + } +} + +// Declare item bounding box for clipping and interaction. +// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface +// declare their minimum size requirement to ItemSize() and then use a larger region for drawing/interaction, which is passed to ItemAdd(). +bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (id != 0) + { + // Navigation processing runs prior to clipping early-out + // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget + // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests unfortunately, but it is still limited to one window. + // it may not scale very well for windows with ten of thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. + // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick) + window->DC.NavLayerActiveMaskNext |= window->DC.NavLayerCurrentMask; + if (g.NavId == id || g.NavAnyRequest) + if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) + if (window == g.NavWindow || ((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened)) + NavProcessItem(window, nav_bb_arg ? *nav_bb_arg : bb, id); + } + + window->DC.LastItemId = id; + window->DC.LastItemRect = bb; + window->DC.LastItemStatusFlags = 0; + + // Clipping test + const bool is_clipped = IsClippedEx(bb, id, false); + if (is_clipped) + return false; + //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] + + // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) + if (IsMouseHoveringRect(bb.Min, bb.Max)) + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HoveredRect; + return true; +} + +// This is roughly matching the behavior of internal-facing ItemHoverable() +// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() +// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId +bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavDisableMouseHover && !g.NavDisableHighlight) + return IsItemFocused(); + + // Test for bounding box overlap, as updated as ItemAdd() + if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) + return false; + IM_ASSERT((flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) == 0); // Flags not supported by this function + + // Test if we are hovering the right window (our window could be behind another window) + // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable to use IsItemHovered() after EndChild() itself. + // Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was the test that has been running for a long while. + //if (g.HoveredWindow != window) + // return false; + if (g.HoveredRootWindow != window->RootWindow && !(flags & ImGuiHoveredFlags_AllowWhenOverlapped)) + return false; + + // Test if another item is active (e.g. being dragged) + if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + if (g.ActiveId != 0 && g.ActiveId != window->DC.LastItemId && !g.ActiveIdAllowOverlap && g.ActiveId != window->MoveId) + return false; + + // Test if interactions on this window are blocked by an active popup or modal + if (!IsWindowContentHoverable(window, flags)) + return false; + + // Test if the item is disabled + if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) + return false; + + // Special handling for the 1st item after Begin() which represent the title bar. When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect tht case. + if (window->DC.LastItemId == window->MoveId && window->WriteAccessed) + return false; + return true; +} + +// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). +bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (g.HoveredWindow != window) + return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + return false; + if (!IsMouseHoveringRect(bb.Min, bb.Max)) + return false; + if (g.NavDisableMouseHover || !IsWindowContentHoverable(window, ImGuiHoveredFlags_Default)) + return false; + if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) + return false; + + SetHoveredID(id); + return true; +} + +bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id, bool clip_even_when_logged) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!bb.Overlaps(window->ClipRect)) + if (id == 0 || id != g.ActiveId) + if (clip_even_when_logged || !g.LogEnabled) + return true; + return false; +} + +bool ImGui::FocusableItemRegister(ImGuiWindow* window, ImGuiID id, bool tab_stop) +{ + ImGuiContext& g = *GImGui; + + const bool allow_keyboard_focus = (window->DC.ItemFlags & (ImGuiItemFlags_AllowKeyboardFocus | ImGuiItemFlags_Disabled)) == ImGuiItemFlags_AllowKeyboardFocus; + window->FocusIdxAllCounter++; + if (allow_keyboard_focus) + window->FocusIdxTabCounter++; + + // Process keyboard input at this point: TAB/Shift-TAB to tab out of the currently focused item. + // Note that we can always TAB out of a widget that doesn't allow tabbing in. + if (tab_stop && (g.ActiveId == id) && window->FocusIdxAllRequestNext == INT_MAX && window->FocusIdxTabRequestNext == INT_MAX && !g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab)) + window->FocusIdxTabRequestNext = window->FocusIdxTabCounter + (g.IO.KeyShift ? (allow_keyboard_focus ? -1 : 0) : +1); // Modulo on index will be applied at the end of frame once we've got the total counter of items. + + if (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent) + return true; + if (allow_keyboard_focus && window->FocusIdxTabCounter == window->FocusIdxTabRequestCurrent) + { + g.NavJustTabbedId = id; + return true; + } + + return false; +} + +void ImGui::FocusableItemUnregister(ImGuiWindow* window) +{ + window->FocusIdxAllCounter--; + window->FocusIdxTabCounter--; +} + +ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_x, float default_y) +{ + ImGuiContext& g = *GImGui; + ImVec2 content_max; + if (size.x < 0.0f || size.y < 0.0f) + content_max = g.CurrentWindow->Pos + GetContentRegionMax(); + if (size.x <= 0.0f) + size.x = (size.x == 0.0f) ? default_x : ImMax(content_max.x - g.CurrentWindow->DC.CursorPos.x, 4.0f) + size.x; + if (size.y <= 0.0f) + size.y = (size.y == 0.0f) ? default_y : ImMax(content_max.y - g.CurrentWindow->DC.CursorPos.y, 4.0f) + size.y; + return size; +} + +float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +{ + if (wrap_pos_x < 0.0f) + return 0.0f; + + ImGuiWindow* window = GetCurrentWindowRead(); + if (wrap_pos_x == 0.0f) + wrap_pos_x = GetContentRegionMax().x + window->Pos.x; + else if (wrap_pos_x > 0.0f) + wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space + + return ImMax(wrap_pos_x - pos.x, 1.0f); +} + +//----------------------------------------------------------------------------- + +void* ImGui::MemAlloc(size_t sz) +{ + GImAllocatorActiveAllocationsCount++; + return GImAllocatorAllocFunc(sz, GImAllocatorUserData); +} + +void ImGui::MemFree(void* ptr) +{ + if (ptr) GImAllocatorActiveAllocationsCount--; + return GImAllocatorFreeFunc(ptr, GImAllocatorUserData); +} + +const char* ImGui::GetClipboardText() +{ + return GImGui->IO.GetClipboardTextFn ? GImGui->IO.GetClipboardTextFn(GImGui->IO.ClipboardUserData) : ""; +} + +void ImGui::SetClipboardText(const char* text) +{ + if (GImGui->IO.SetClipboardTextFn) + GImGui->IO.SetClipboardTextFn(GImGui->IO.ClipboardUserData, text); +} + +const char* ImGui::GetVersion() +{ + return IMGUI_VERSION; +} + +// Internal state access - if you want to share ImGui state between modules (e.g. DLL) or allocate it yourself +// Note that we still point to some static data and members (such as GFontAtlas), so the state instance you end up using will point to the static data within its module +ImGuiContext* ImGui::GetCurrentContext() +{ + return GImGui; +} + +void ImGui::SetCurrentContext(ImGuiContext* ctx) +{ +#ifdef IMGUI_SET_CURRENT_CONTEXT_FUNC + IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. +#else + GImGui = ctx; +#endif +} + +void ImGui::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void(*free_func)(void* ptr, void* user_data), void* user_data) +{ + GImAllocatorAllocFunc = alloc_func; + GImAllocatorFreeFunc = free_func; + GImAllocatorUserData = user_data; +} + +ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) +{ + ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); + if (GImGui == NULL) + SetCurrentContext(ctx); + Initialize(ctx); + return ctx; +} + +void ImGui::DestroyContext(ImGuiContext* ctx) +{ + if (ctx == NULL) + ctx = GImGui; + Shutdown(ctx); + if (GImGui == ctx) + SetCurrentContext(NULL); + IM_DELETE(ctx); +} + +ImGuiIO& ImGui::GetIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); + return GImGui->IO; +} + +ImGuiStyle& ImGui::GetStyle() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); + return GImGui->Style; +} + +// Same value as passed to the old io.RenderDrawListsFn function. Valid after Render() and until the next call to NewFrame() +ImDrawData* ImGui::GetDrawData() +{ + ImGuiContext& g = *GImGui; + return g.DrawData.Valid ? &g.DrawData : NULL; +} + +float ImGui::GetTime() +{ + return GImGui->Time; +} + +int ImGui::GetFrameCount() +{ + return GImGui->FrameCount; +} + +ImDrawList* ImGui::GetOverlayDrawList() +{ + return &GImGui->OverlayDrawList; +} + +ImDrawListSharedData* ImGui::GetDrawListSharedData() +{ + return &GImGui->DrawListSharedData; +} + +// This needs to be called before we submit any widget (aka in or before Begin) +void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == g.NavWindow); + bool init_for_nav = false; + if (!(window->Flags & ImGuiWindowFlags_NoNavInputs)) + if (!(window->Flags & ImGuiWindowFlags_ChildWindow) || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) + init_for_nav = true; + if (init_for_nav) + { + SetNavID(0, g.NavLayer); + g.NavInitRequest = true; + g.NavInitRequestFromMove = false; + g.NavInitResultId = 0; + g.NavInitResultRectRel = ImRect(); + NavUpdateAnyRequestFlag(); + } + else + { + g.NavId = window->NavLastIds[0]; + } +} + +static ImVec2 NavCalcPreferredMousePos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + if (!window) + return g.IO.MousePos; + const ImRect& rect_rel = window->NavRectRel[g.NavLayer]; + ImVec2 pos = g.NavWindow->Pos + ImVec2(rect_rel.Min.x + ImMin(g.Style.FramePadding.x*4, rect_rel.GetWidth()), rect_rel.Max.y - ImMin(g.Style.FramePadding.y, rect_rel.GetHeight())); + ImRect visible_rect = GetViewportRect(); + return ImFloor(ImClamp(pos, visible_rect.Min, visible_rect.Max)); // ImFloor() is important because non-integer mouse position application in back-end might be lossy and result in undesirable non-zero delta. +} + +static int FindWindowIndex(ImGuiWindow* window) // FIXME-OPT O(N) +{ + ImGuiContext& g = *GImGui; + for (int i = g.Windows.Size-1; i >= 0; i--) + if (g.Windows[i] == window) + return i; + return -1; +} + +static ImGuiWindow* FindWindowNavigable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) +{ + ImGuiContext& g = *GImGui; + for (int i = i_start; i >= 0 && i < g.Windows.Size && i != i_stop; i += dir) + if (ImGui::IsWindowNavFocusable(g.Windows[i])) + return g.Windows[i]; + return NULL; +} + +float ImGui::GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode) +{ + ImGuiContext& g = *GImGui; + if (mode == ImGuiInputReadMode_Down) + return g.IO.NavInputs[n]; // Instant, read analog input (0.0f..1.0f, as provided by user) + + const float t = g.IO.NavInputsDownDuration[n]; + if (t < 0.0f && mode == ImGuiInputReadMode_Released) // Return 1.0f when just released, no repeat, ignore analog input. + return (g.IO.NavInputsDownDurationPrev[n] >= 0.0f ? 1.0f : 0.0f); + if (t < 0.0f) + return 0.0f; + if (mode == ImGuiInputReadMode_Pressed) // Return 1.0f when just pressed, no repeat, ignore analog input. + return (t == 0.0f) ? 1.0f : 0.0f; + if (mode == ImGuiInputReadMode_Repeat) + return (float)CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 0.80f, g.IO.KeyRepeatRate * 0.80f); + if (mode == ImGuiInputReadMode_RepeatSlow) + return (float)CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 1.00f, g.IO.KeyRepeatRate * 2.00f); + if (mode == ImGuiInputReadMode_RepeatFast) + return (float)CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 0.80f, g.IO.KeyRepeatRate * 0.30f); + return 0.0f; +} + +// Equivalent of IsKeyDown() for NavInputs[] +static bool IsNavInputDown(ImGuiNavInput n) +{ + return GImGui->IO.NavInputs[n] > 0.0f; +} + +// Equivalent of IsKeyPressed() for NavInputs[] +static bool IsNavInputPressed(ImGuiNavInput n, ImGuiInputReadMode mode) +{ + return ImGui::GetNavInputAmount(n, mode) > 0.0f; +} + +static bool IsNavInputPressedAnyOfTwo(ImGuiNavInput n1, ImGuiNavInput n2, ImGuiInputReadMode mode) +{ + return (ImGui::GetNavInputAmount(n1, mode) + ImGui::GetNavInputAmount(n2, mode)) > 0.0f; +} + +ImVec2 ImGui::GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor, float fast_factor) +{ + ImVec2 delta(0.0f, 0.0f); + if (dir_sources & ImGuiNavDirSourceFlags_Keyboard) + delta += ImVec2(GetNavInputAmount(ImGuiNavInput_KeyRight_, mode) - GetNavInputAmount(ImGuiNavInput_KeyLeft_, mode), GetNavInputAmount(ImGuiNavInput_KeyDown_, mode) - GetNavInputAmount(ImGuiNavInput_KeyUp_, mode)); + if (dir_sources & ImGuiNavDirSourceFlags_PadDPad) + delta += ImVec2(GetNavInputAmount(ImGuiNavInput_DpadRight, mode) - GetNavInputAmount(ImGuiNavInput_DpadLeft, mode), GetNavInputAmount(ImGuiNavInput_DpadDown, mode) - GetNavInputAmount(ImGuiNavInput_DpadUp, mode)); + if (dir_sources & ImGuiNavDirSourceFlags_PadLStick) + delta += ImVec2(GetNavInputAmount(ImGuiNavInput_LStickRight, mode) - GetNavInputAmount(ImGuiNavInput_LStickLeft, mode), GetNavInputAmount(ImGuiNavInput_LStickDown, mode) - GetNavInputAmount(ImGuiNavInput_LStickUp, mode)); + if (slow_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakSlow)) + delta *= slow_factor; + if (fast_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakFast)) + delta *= fast_factor; + return delta; +} + +static void NavUpdateWindowingHighlightWindow(int focus_change_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget); + if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) + return; + + const int i_current = FindWindowIndex(g.NavWindowingTarget); + ImGuiWindow* window_target = FindWindowNavigable(i_current + focus_change_dir, -INT_MAX, focus_change_dir); + if (!window_target) + window_target = FindWindowNavigable((focus_change_dir < 0) ? (g.Windows.Size - 1) : 0, i_current, focus_change_dir); + g.NavWindowingTarget = window_target; + g.NavWindowingToggleLayer = false; +} + +// Window management mode (hold to: change focus/move/resize, tap to: toggle menu layer) +static void ImGui::NavUpdateWindowing() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* apply_focus_window = NULL; + bool apply_toggle_layer = false; + + bool start_windowing_with_gamepad = !g.NavWindowingTarget && IsNavInputPressed(ImGuiNavInput_Menu, ImGuiInputReadMode_Pressed); + bool start_windowing_with_keyboard = !g.NavWindowingTarget && g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab) && (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard); + if (start_windowing_with_gamepad || start_windowing_with_keyboard) + if (ImGuiWindow* window = g.NavWindow ? g.NavWindow : FindWindowNavigable(g.Windows.Size - 1, -INT_MAX, -1)) + { + g.NavWindowingTarget = window->RootWindowForTabbing; + g.NavWindowingHighlightTimer = g.NavWindowingHighlightAlpha = 0.0f; + g.NavWindowingToggleLayer = start_windowing_with_keyboard ? false : true; + g.NavWindowingInputSource = start_windowing_with_keyboard ? ImGuiInputSource_NavKeyboard : ImGuiInputSource_NavGamepad; + } + + // Gamepad update + g.NavWindowingHighlightTimer += g.IO.DeltaTime; + if (g.NavWindowingTarget && g.NavWindowingInputSource == ImGuiInputSource_NavGamepad) + { + // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingHighlightTimer - 0.20f) / 0.05f)); + + // Select window to focus + const int focus_change_dir = (int)IsNavInputPressed(ImGuiNavInput_FocusPrev, ImGuiInputReadMode_RepeatSlow) - (int)IsNavInputPressed(ImGuiNavInput_FocusNext, ImGuiInputReadMode_RepeatSlow); + if (focus_change_dir != 0) + { + NavUpdateWindowingHighlightWindow(focus_change_dir); + g.NavWindowingHighlightAlpha = 1.0f; + } + + // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered front-most) + if (!IsNavInputDown(ImGuiNavInput_Menu)) + { + g.NavWindowingToggleLayer &= (g.NavWindowingHighlightAlpha < 1.0f); // Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore. + if (g.NavWindowingToggleLayer && g.NavWindow) + apply_toggle_layer = true; + else if (!g.NavWindowingToggleLayer) + apply_focus_window = g.NavWindowingTarget; + g.NavWindowingTarget = NULL; + } + } + + // Keyboard: Focus + if (g.NavWindowingTarget && g.NavWindowingInputSource == ImGuiInputSource_NavKeyboard) + { + // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingHighlightTimer - 0.15f) / 0.04f)); // 1.0f + if (IsKeyPressedMap(ImGuiKey_Tab, true)) + NavUpdateWindowingHighlightWindow(g.IO.KeyShift ? +1 : -1); + if (!g.IO.KeyCtrl) + apply_focus_window = g.NavWindowingTarget; + } + + // Keyboard: Press and Release ALT to toggle menu layer + // FIXME: We lack an explicit IO variable for "is the imgui window focused", so compare mouse validity to detect the common case of back-end clearing releases all keys on ALT-TAB + if ((g.ActiveId == 0 || g.ActiveIdAllowOverlap) && IsNavInputPressed(ImGuiNavInput_KeyMenu_, ImGuiInputReadMode_Released)) + if (IsMousePosValid(&g.IO.MousePos) == IsMousePosValid(&g.IO.MousePosPrev)) + apply_toggle_layer = true; + + // Move window + if (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoMove)) + { + ImVec2 move_delta; + if (g.NavWindowingInputSource == ImGuiInputSource_NavKeyboard && !g.IO.KeyShift) + move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard, ImGuiInputReadMode_Down); + if (g.NavWindowingInputSource == ImGuiInputSource_NavGamepad) + move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down); + if (move_delta.x != 0.0f || move_delta.y != 0.0f) + { + const float NAV_MOVE_SPEED = 800.0f; + const float move_speed = ImFloor(NAV_MOVE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); + g.NavWindowingTarget->PosFloat += move_delta * move_speed; + g.NavDisableMouseHover = true; + MarkIniSettingsDirty(g.NavWindowingTarget); + } + } + + // Apply final focus + if (apply_focus_window && (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindowForTabbing)) + { + g.NavDisableHighlight = false; + g.NavDisableMouseHover = true; + apply_focus_window = NavRestoreLastChildNavWindow(apply_focus_window); + ClosePopupsOverWindow(apply_focus_window); + FocusWindow(apply_focus_window); + if (apply_focus_window->NavLastIds[0] == 0) + NavInitWindow(apply_focus_window, false); + + // If the window only has a menu layer, select it directly + if (apply_focus_window->DC.NavLayerActiveMask == (1 << 1)) + g.NavLayer = 1; + } + if (apply_focus_window) + g.NavWindowingTarget = NULL; + + // Apply menu/layer toggle + if (apply_toggle_layer && g.NavWindow) + { + ImGuiWindow* new_nav_window = g.NavWindow; + while ((new_nav_window->DC.NavLayerActiveMask & (1 << 1)) == 0 && (new_nav_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (new_nav_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + new_nav_window = new_nav_window->ParentWindow; + if (new_nav_window != g.NavWindow) + { + ImGuiWindow* old_nav_window = g.NavWindow; + FocusWindow(new_nav_window); + new_nav_window->NavLastChildNavWindow = old_nav_window; + } + g.NavDisableHighlight = false; + g.NavDisableMouseHover = true; + NavRestoreLayer((g.NavWindow->DC.NavLayerActiveMask & (1 << 1)) ? (g.NavLayer ^ 1) : 0); + } +} + +// NB: We modify rect_rel by the amount we scrolled for, so it is immediately updated. +static void NavScrollToBringItemIntoView(ImGuiWindow* window, ImRect& item_rect_rel) +{ + // Scroll to keep newly navigated item fully into view + ImRect window_rect_rel(window->InnerRect.Min - window->Pos - ImVec2(1, 1), window->InnerRect.Max - window->Pos + ImVec2(1, 1)); + //g.OverlayDrawList.AddRect(window->Pos + window_rect_rel.Min, window->Pos + window_rect_rel.Max, IM_COL32_WHITE); // [DEBUG] + if (window_rect_rel.Contains(item_rect_rel)) + return; + + ImGuiContext& g = *GImGui; + if (window->ScrollbarX && item_rect_rel.Min.x < window_rect_rel.Min.x) + { + window->ScrollTarget.x = item_rect_rel.Min.x + window->Scroll.x - g.Style.ItemSpacing.x; + window->ScrollTargetCenterRatio.x = 0.0f; + } + else if (window->ScrollbarX && item_rect_rel.Max.x >= window_rect_rel.Max.x) + { + window->ScrollTarget.x = item_rect_rel.Max.x + window->Scroll.x + g.Style.ItemSpacing.x; + window->ScrollTargetCenterRatio.x = 1.0f; + } + if (item_rect_rel.Min.y < window_rect_rel.Min.y) + { + window->ScrollTarget.y = item_rect_rel.Min.y + window->Scroll.y - g.Style.ItemSpacing.y; + window->ScrollTargetCenterRatio.y = 0.0f; + } + else if (item_rect_rel.Max.y >= window_rect_rel.Max.y) + { + window->ScrollTarget.y = item_rect_rel.Max.y + window->Scroll.y + g.Style.ItemSpacing.y; + window->ScrollTargetCenterRatio.y = 1.0f; + } + + // Estimate upcoming scroll so we can offset our relative mouse position so mouse position can be applied immediately (under this block) + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + item_rect_rel.Translate(window->Scroll - next_scroll); +} + +static void ImGui::NavUpdate() +{ + ImGuiContext& g = *GImGui; + g.IO.WantMoveMouse = false; + +#if 0 + if (g.NavScoringCount > 0) printf("[%05d] NavScoringCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.FrameCount, g.NavScoringCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); +#endif + + // Update Keyboard->Nav inputs mapping + memset(g.IO.NavInputs + ImGuiNavInput_InternalStart_, 0, (ImGuiNavInput_COUNT - ImGuiNavInput_InternalStart_) * sizeof(g.IO.NavInputs[0])); + if (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard) + { + #define NAV_MAP_KEY(_KEY, _NAV_INPUT) if (g.IO.KeyMap[_KEY] != -1 && IsKeyDown(g.IO.KeyMap[_KEY])) g.IO.NavInputs[_NAV_INPUT] = 1.0f; + NAV_MAP_KEY(ImGuiKey_Space, ImGuiNavInput_Activate ); + NAV_MAP_KEY(ImGuiKey_Enter, ImGuiNavInput_Input ); + NAV_MAP_KEY(ImGuiKey_Escape, ImGuiNavInput_Cancel ); + NAV_MAP_KEY(ImGuiKey_LeftArrow, ImGuiNavInput_KeyLeft_ ); + NAV_MAP_KEY(ImGuiKey_RightArrow,ImGuiNavInput_KeyRight_); + NAV_MAP_KEY(ImGuiKey_UpArrow, ImGuiNavInput_KeyUp_ ); + NAV_MAP_KEY(ImGuiKey_DownArrow, ImGuiNavInput_KeyDown_ ); + if (g.IO.KeyCtrl) g.IO.NavInputs[ImGuiNavInput_TweakSlow] = 1.0f; + if (g.IO.KeyShift) g.IO.NavInputs[ImGuiNavInput_TweakFast] = 1.0f; + if (g.IO.KeyAlt) g.IO.NavInputs[ImGuiNavInput_KeyMenu_] = 1.0f; +#undef NAV_MAP_KEY + } + + memcpy(g.IO.NavInputsDownDurationPrev, g.IO.NavInputsDownDuration, sizeof(g.IO.NavInputsDownDuration)); + for (int i = 0; i < IM_ARRAYSIZE(g.IO.NavInputs); i++) + g.IO.NavInputsDownDuration[i] = (g.IO.NavInputs[i] > 0.0f) ? (g.IO.NavInputsDownDuration[i] < 0.0f ? 0.0f : g.IO.NavInputsDownDuration[i] + g.IO.DeltaTime) : -1.0f; + + // Process navigation init request (select first/default focus) + if (g.NavInitResultId != 0 && (!g.NavDisableHighlight || g.NavInitRequestFromMove)) + { + // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) + IM_ASSERT(g.NavWindow); + if (g.NavInitRequestFromMove) + SetNavIDAndMoveMouse(g.NavInitResultId, g.NavLayer, g.NavInitResultRectRel); + else + SetNavID(g.NavInitResultId, g.NavLayer); + g.NavWindow->NavRectRel[g.NavLayer] = g.NavInitResultRectRel; + } + g.NavInitRequest = false; + g.NavInitRequestFromMove = false; + g.NavInitResultId = 0; + g.NavJustMovedToId = 0; + + // Process navigation move request + if (g.NavMoveRequest && (g.NavMoveResultLocal.ID != 0 || g.NavMoveResultOther.ID != 0)) + { + // Select which result to use + ImGuiNavMoveResult* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; + if (g.NavMoveResultOther.ID != 0 && g.NavMoveResultOther.Window->ParentWindow == g.NavWindow) // Maybe entering a flattened child? In this case solve the tie using the regular scoring rules + if ((g.NavMoveResultOther.DistBox < g.NavMoveResultLocal.DistBox) || (g.NavMoveResultOther.DistBox == g.NavMoveResultLocal.DistBox && g.NavMoveResultOther.DistCenter < g.NavMoveResultLocal.DistCenter)) + result = &g.NavMoveResultOther; + + IM_ASSERT(g.NavWindow && result->Window); + + // Scroll to keep newly navigated item fully into view + if (g.NavLayer == 0) + NavScrollToBringItemIntoView(result->Window, result->RectRel); + + // Apply result from previous frame navigation directional move request + ClearActiveID(); + g.NavWindow = result->Window; + SetNavIDAndMoveMouse(result->ID, g.NavLayer, result->RectRel); + g.NavJustMovedToId = result->ID; + g.NavMoveFromClampedRefRect = false; + } + + // When a forwarded move request failed, we restore the highlight that we disabled during the forward frame + if (g.NavMoveRequestForward == ImGuiNavForward_ForwardActive) + { + IM_ASSERT(g.NavMoveRequest); + if (g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0) + g.NavDisableHighlight = false; + g.NavMoveRequestForward = ImGuiNavForward_None; + } + + // Apply application mouse position movement, after we had a chance to process move request result. + if (g.NavMousePosDirty && g.NavIdIsAlive) + { + // Set mouse position given our knowledge of the nav widget position from last frame + if (g.IO.NavFlags & ImGuiNavFlags_MoveMouse) + { + g.IO.MousePos = g.IO.MousePosPrev = NavCalcPreferredMousePos(); + g.IO.WantMoveMouse = true; + } + g.NavMousePosDirty = false; + } + g.NavIdIsAlive = false; + g.NavJustTabbedId = 0; + IM_ASSERT(g.NavLayer == 0 || g.NavLayer == 1); + + // Store our return window (for returning from Layer 1 to Layer 0) and clear it as soon as we step back in our own Layer 0 + if (g.NavWindow) + NavSaveLastChildNavWindow(g.NavWindow); + if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == 0) + g.NavWindow->NavLastChildNavWindow = NULL; + + NavUpdateWindowing(); + + // Set output flags for user application + g.IO.NavActive = (g.IO.NavFlags & (ImGuiNavFlags_EnableGamepad | ImGuiNavFlags_EnableKeyboard)) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); + g.IO.NavVisible = (g.IO.NavActive && g.NavId != 0 && !g.NavDisableHighlight) || (g.NavWindowingTarget != NULL) || g.NavInitRequest; + + // Process NavCancel input (to close a popup, get back to parent, clear focus) + if (IsNavInputPressed(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed)) + { + if (g.ActiveId != 0) + { + ClearActiveID(); + } + else if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow) && !(g.NavWindow->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->ParentWindow) + { + // Exit child window + ImGuiWindow* child_window = g.NavWindow; + ImGuiWindow* parent_window = g.NavWindow->ParentWindow; + IM_ASSERT(child_window->ChildId != 0); + FocusWindow(parent_window); + SetNavID(child_window->ChildId, 0); + g.NavIdIsAlive = false; + if (g.NavDisableMouseHover) + g.NavMousePosDirty = true; + } + else if (g.OpenPopupStack.Size > 0) + { + // Close open popup/menu + if (!(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) + ClosePopupToLevel(g.OpenPopupStack.Size - 1); + } + else if (g.NavLayer != 0) + { + // Leave the "menu" layer + NavRestoreLayer(0); + } + else + { + // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were + if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup) || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) + g.NavWindow->NavLastIds[0] = 0; + g.NavId = 0; + } + } + + // Process manual activation request + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavInputId = 0; + if (g.NavId != 0 && !g.NavDisableHighlight && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + { + bool activate_down = IsNavInputDown(ImGuiNavInput_Activate); + bool activate_pressed = activate_down && IsNavInputPressed(ImGuiNavInput_Activate, ImGuiInputReadMode_Pressed); + if (g.ActiveId == 0 && activate_pressed) + g.NavActivateId = g.NavId; + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_down) + g.NavActivateDownId = g.NavId; + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_pressed) + g.NavActivatePressedId = g.NavId; + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && IsNavInputPressed(ImGuiNavInput_Input, ImGuiInputReadMode_Pressed)) + g.NavInputId = g.NavId; + } + if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + g.NavDisableHighlight = true; + if (g.NavActivateId != 0) + IM_ASSERT(g.NavActivateDownId == g.NavActivateId); + g.NavMoveRequest = false; + + // Process programmatic activation request + if (g.NavNextActivateId != 0) + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavInputId = g.NavNextActivateId; + g.NavNextActivateId = 0; + + // Initiate directional inputs request + const int allowed_dir_flags = (g.ActiveId == 0) ? ~0 : g.ActiveIdAllowNavDirFlags; + if (g.NavMoveRequestForward == ImGuiNavForward_None) + { + g.NavMoveDir = ImGuiDir_None; + if (g.NavWindow && !g.NavWindowingTarget && allowed_dir_flags && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + { + if ((allowed_dir_flags & (1<Flags & ImGuiWindowFlags_NoNavInputs) && !g.NavWindowingTarget) + { + // *Fallback* manual-scroll with NavUp/NavDown when window has no navigable item + ImGuiWindow* window = g.NavWindow; + const float scroll_speed = ImFloor(window->CalcFontSize() * 100 * g.IO.DeltaTime + 0.5f); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. + if (window->DC.NavLayerActiveMask == 0x00 && window->DC.NavHasScroll && g.NavMoveRequest) + { + if (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) + SetWindowScrollX(window, ImFloor(window->Scroll.x + ((g.NavMoveDir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); + if (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) + SetWindowScrollY(window, ImFloor(window->Scroll.y + ((g.NavMoveDir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); + } + + // *Normal* Manual scroll with NavScrollXXX keys + // Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds. + ImVec2 scroll_dir = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down, 1.0f/10.0f, 10.0f); + if (scroll_dir.x != 0.0f && window->ScrollbarX) + { + SetWindowScrollX(window, ImFloor(window->Scroll.x + scroll_dir.x * scroll_speed)); + g.NavMoveFromClampedRefRect = true; + } + if (scroll_dir.y != 0.0f) + { + SetWindowScrollY(window, ImFloor(window->Scroll.y + scroll_dir.y * scroll_speed)); + g.NavMoveFromClampedRefRect = true; + } + } + + // Reset search results + g.NavMoveResultLocal.Clear(); + g.NavMoveResultOther.Clear(); + + // When we have manually scrolled (without using navigation) and NavId becomes out of bounds, we project its bounding box to the visible area to restart navigation within visible items + if (g.NavMoveRequest && g.NavMoveFromClampedRefRect && g.NavLayer == 0) + { + ImGuiWindow* window = g.NavWindow; + ImRect window_rect_rel(window->InnerRect.Min - window->Pos - ImVec2(1,1), window->InnerRect.Max - window->Pos + ImVec2(1,1)); + if (!window_rect_rel.Contains(window->NavRectRel[g.NavLayer])) + { + float pad = window->CalcFontSize() * 0.5f; + window_rect_rel.Expand(ImVec2(-ImMin(window_rect_rel.GetWidth(), pad), -ImMin(window_rect_rel.GetHeight(), pad))); // Terrible approximation for the intent of starting navigation from first fully visible item + window->NavRectRel[g.NavLayer].ClipWith(window_rect_rel); + g.NavId = 0; + } + g.NavMoveFromClampedRefRect = false; + } + + // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) + ImRect nav_rect_rel = (g.NavWindow && g.NavWindow->NavRectRel[g.NavLayer].IsFinite()) ? g.NavWindow->NavRectRel[g.NavLayer] : ImRect(0,0,0,0); + g.NavScoringRectScreen = g.NavWindow ? ImRect(g.NavWindow->Pos + nav_rect_rel.Min, g.NavWindow->Pos + nav_rect_rel.Max) : GetViewportRect(); + g.NavScoringRectScreen.Min.x = ImMin(g.NavScoringRectScreen.Min.x + 1.0f, g.NavScoringRectScreen.Max.x); + g.NavScoringRectScreen.Max.x = g.NavScoringRectScreen.Min.x; + IM_ASSERT(!g.NavScoringRectScreen.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allows us to remove extraneous fabsf() calls in NavScoreItem(). + //g.OverlayDrawList.AddRect(g.NavScoringRectScreen.Min, g.NavScoringRectScreen.Max, IM_COL32(255,200,0,255)); // [DEBUG] + g.NavScoringCount = 0; +#if IMGUI_DEBUG_NAV_RECTS + if (g.NavWindow) { for (int layer = 0; layer < 2; layer++) g.OverlayDrawList.AddRect(g.NavWindow->Pos + g.NavWindow->NavRectRel[layer].Min, g.NavWindow->Pos + g.NavWindow->NavRectRel[layer].Max, IM_COL32(255,200,0,255)); } // [DEBUG] + if (g.NavWindow) { ImU32 col = (g.NavWindow->HiddenFrames <= 0) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredMousePos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); g.OverlayDrawList.AddCircleFilled(p, 3.0f, col); g.OverlayDrawList.AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); } +#endif +} + +static void ImGui::UpdateMovingWindow() +{ + ImGuiContext& g = *GImGui; + if (g.MovingWindow && g.MovingWindow->MoveId == g.ActiveId && g.ActiveIdSource == ImGuiInputSource_Mouse) + { + // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). + // We track it to preserve Focus and so that ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. + KeepAliveID(g.ActiveId); + IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); + ImGuiWindow* moving_window = g.MovingWindow->RootWindow; + if (g.IO.MouseDown[0]) + { + ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; + if (moving_window->PosFloat.x != pos.x || moving_window->PosFloat.y != pos.y) + { + MarkIniSettingsDirty(moving_window); + moving_window->PosFloat = pos; + } + FocusWindow(g.MovingWindow); + } + else + { + ClearActiveID(); + g.MovingWindow = NULL; + } + } + else + { + // When clicking/dragging from a window that has the _NoMove flag, we still set the ActiveId in order to prevent hovering others. + if (g.ActiveIdWindow && g.ActiveIdWindow->MoveId == g.ActiveId) + { + KeepAliveID(g.ActiveId); + if (!g.IO.MouseDown[0]) + ClearActiveID(); + } + g.MovingWindow = NULL; + } +} + +void ImGui::NewFrame() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); + ImGuiContext& g = *GImGui; + + // Check user data + // (We pass an error message in the assert expression as a trick to get it visible to programmers who are not using a debugger, as most assert handlers display their argument) + IM_ASSERT(g.Initialized); + IM_ASSERT(g.IO.DeltaTime >= 0.0f && "Need a positive DeltaTime (zero is tolerated but will cause some timing issues)"); + IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value"); + IM_ASSERT(g.IO.Fonts->Fonts.Size > 0 && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8() ?"); + IM_ASSERT(g.IO.Fonts->Fonts[0]->IsLoaded() && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8() ?"); + IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting"); + IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting. Alpha cannot be negative (allows us to avoid a few clamps in color computations)"); + IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); + for (int n = 0; n < ImGuiKey_COUNT; n++) + IM_ASSERT(g.IO.KeyMap[n] >= -1 && g.IO.KeyMap[n] < IM_ARRAYSIZE(g.IO.KeysDown) && "io.KeyMap[] contains an out of bound value (need to be 0..512, or -1 for unmapped key)"); + + // Do a simple check for required key mapping (we intentionally do NOT check all keys to not pressure user into setting up everything, but Space is required and was super recently added in 1.60 WIP) + if (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard) + IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation."); + + // Load settings on first frame + if (!g.SettingsLoaded) + { + IM_ASSERT(g.SettingsWindows.empty()); + LoadIniSettingsFromDisk(g.IO.IniFilename); + g.SettingsLoaded = true; + } + + g.Time += g.IO.DeltaTime; + g.FrameCount += 1; + g.TooltipOverrideCount = 0; + g.WindowsActiveCount = 0; + + SetCurrentFont(GetDefaultFont()); + IM_ASSERT(g.Font->IsLoaded()); + g.DrawListSharedData.ClipRectFullscreen = ImVec4(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); + g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; + + g.OverlayDrawList.Clear(); + g.OverlayDrawList.PushTextureID(g.IO.Fonts->TexID); + g.OverlayDrawList.PushClipRectFullScreen(); + g.OverlayDrawList.Flags = (g.Style.AntiAliasedLines ? ImDrawListFlags_AntiAliasedLines : 0) | (g.Style.AntiAliasedFill ? ImDrawListFlags_AntiAliasedFill : 0); + + // Mark rendering data as invalid to prevent user who may have a handle on it to use it + g.DrawData.Clear(); + + // Clear reference to active widget if the widget isn't alive anymore + if (!g.HoveredIdPreviousFrame) + g.HoveredIdTimer = 0.0f; + g.HoveredIdPreviousFrame = g.HoveredId; + g.HoveredId = 0; + g.HoveredIdAllowOverlap = false; + if (!g.ActiveIdIsAlive && g.ActiveIdPreviousFrame == g.ActiveId && g.ActiveId != 0) + ClearActiveID(); + if (g.ActiveId) + g.ActiveIdTimer += g.IO.DeltaTime; + g.ActiveIdPreviousFrame = g.ActiveId; + g.ActiveIdIsAlive = false; + g.ActiveIdIsJustActivated = false; + if (g.ScalarAsInputTextId && g.ActiveId != g.ScalarAsInputTextId) + g.ScalarAsInputTextId = 0; + + // Elapse drag & drop payload + if (g.DragDropActive && g.DragDropPayload.DataFrameCount + 1 < g.FrameCount) + { + ClearDragDrop(); + g.DragDropPayloadBufHeap.clear(); + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); + } + g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr; + g.DragDropAcceptIdCurr = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + + // Update keyboard input state + memcpy(g.IO.KeysDownDurationPrev, g.IO.KeysDownDuration, sizeof(g.IO.KeysDownDuration)); + for (int i = 0; i < IM_ARRAYSIZE(g.IO.KeysDown); i++) + g.IO.KeysDownDuration[i] = g.IO.KeysDown[i] ? (g.IO.KeysDownDuration[i] < 0.0f ? 0.0f : g.IO.KeysDownDuration[i] + g.IO.DeltaTime) : -1.0f; + + // Update gamepad/keyboard directional navigation + NavUpdate(); + + // Update mouse input state + // If mouse just appeared or disappeared (usually denoted by -FLT_MAX component, but in reality we test for -256000.0f) we cancel out movement in MouseDelta + if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MousePosPrev)) + g.IO.MouseDelta = g.IO.MousePos - g.IO.MousePosPrev; + else + g.IO.MouseDelta = ImVec2(0.0f, 0.0f); + if (g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f) + g.NavDisableMouseHover = false; + + g.IO.MousePosPrev = g.IO.MousePos; + for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) + { + g.IO.MouseClicked[i] = g.IO.MouseDown[i] && g.IO.MouseDownDuration[i] < 0.0f; + g.IO.MouseReleased[i] = !g.IO.MouseDown[i] && g.IO.MouseDownDuration[i] >= 0.0f; + g.IO.MouseDownDurationPrev[i] = g.IO.MouseDownDuration[i]; + g.IO.MouseDownDuration[i] = g.IO.MouseDown[i] ? (g.IO.MouseDownDuration[i] < 0.0f ? 0.0f : g.IO.MouseDownDuration[i] + g.IO.DeltaTime) : -1.0f; + g.IO.MouseDoubleClicked[i] = false; + if (g.IO.MouseClicked[i]) + { + if (g.Time - g.IO.MouseClickedTime[i] < g.IO.MouseDoubleClickTime) + { + if (ImLengthSqr(g.IO.MousePos - g.IO.MouseClickedPos[i]) < g.IO.MouseDoubleClickMaxDist * g.IO.MouseDoubleClickMaxDist) + g.IO.MouseDoubleClicked[i] = true; + g.IO.MouseClickedTime[i] = -FLT_MAX; // so the third click isn't turned into a double-click + } + else + { + g.IO.MouseClickedTime[i] = g.Time; + } + g.IO.MouseClickedPos[i] = g.IO.MousePos; + g.IO.MouseDragMaxDistanceAbs[i] = ImVec2(0.0f, 0.0f); + g.IO.MouseDragMaxDistanceSqr[i] = 0.0f; + } + else if (g.IO.MouseDown[i]) + { + ImVec2 mouse_delta = g.IO.MousePos - g.IO.MouseClickedPos[i]; + g.IO.MouseDragMaxDistanceAbs[i].x = ImMax(g.IO.MouseDragMaxDistanceAbs[i].x, mouse_delta.x < 0.0f ? -mouse_delta.x : mouse_delta.x); + g.IO.MouseDragMaxDistanceAbs[i].y = ImMax(g.IO.MouseDragMaxDistanceAbs[i].y, mouse_delta.y < 0.0f ? -mouse_delta.y : mouse_delta.y); + g.IO.MouseDragMaxDistanceSqr[i] = ImMax(g.IO.MouseDragMaxDistanceSqr[i], ImLengthSqr(mouse_delta)); + } + if (g.IO.MouseClicked[i]) // Clicking any mouse button reactivate mouse hovering which may have been deactivated by gamepad/keyboard navigation + g.NavDisableMouseHover = false; + } + + // Calculate frame-rate for the user, as a purely luxurious feature + g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; + g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; + g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); + g.IO.Framerate = 1.0f / (g.FramerateSecPerFrameAccum / (float)IM_ARRAYSIZE(g.FramerateSecPerFrame)); + + // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) + UpdateMovingWindow(); + + // Delay saving settings so we don't spam disk too much + if (g.SettingsDirtyTimer > 0.0f) + { + g.SettingsDirtyTimer -= g.IO.DeltaTime; + if (g.SettingsDirtyTimer <= 0.0f) + SaveIniSettingsToDisk(g.IO.IniFilename); + } + + // Find the window we are hovering + // - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow. + // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point. + // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. + g.HoveredWindow = (g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoInputs)) ? g.MovingWindow : FindHoveredWindow(); + g.HoveredRootWindow = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; + + ImGuiWindow* modal_window = GetFrontMostModalRootWindow(); + if (modal_window != NULL) + { + g.ModalWindowDarkeningRatio = ImMin(g.ModalWindowDarkeningRatio + g.IO.DeltaTime * 6.0f, 1.0f); + if (g.HoveredRootWindow && !IsWindowChildOf(g.HoveredRootWindow, modal_window)) + g.HoveredRootWindow = g.HoveredWindow = NULL; + } + else + { + g.ModalWindowDarkeningRatio = 0.0f; + } + + // Update the WantCaptureMouse/WantCaptureKeyboard flags, so user can capture/discard the inputs away from the rest of their application. + // When clicking outside of a window we assume the click is owned by the application and won't request capture. We need to track click ownership. + int mouse_earliest_button_down = -1; + bool mouse_any_down = false; + for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) + { + if (g.IO.MouseClicked[i]) + g.IO.MouseDownOwned[i] = (g.HoveredWindow != NULL) || (!g.OpenPopupStack.empty()); + mouse_any_down |= g.IO.MouseDown[i]; + if (g.IO.MouseDown[i]) + if (mouse_earliest_button_down == -1 || g.IO.MouseClickedTime[i] < g.IO.MouseClickedTime[mouse_earliest_button_down]) + mouse_earliest_button_down = i; + } + bool mouse_avail_to_imgui = (mouse_earliest_button_down == -1) || g.IO.MouseDownOwned[mouse_earliest_button_down]; + if (g.WantCaptureMouseNextFrame != -1) + g.IO.WantCaptureMouse = (g.WantCaptureMouseNextFrame != 0); + else + g.IO.WantCaptureMouse = (mouse_avail_to_imgui && (g.HoveredWindow != NULL || mouse_any_down)) || (!g.OpenPopupStack.empty()); + + if (g.WantCaptureKeyboardNextFrame != -1) + g.IO.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); + else + g.IO.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); + if (g.IO.NavActive && (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard) && !(g.IO.NavFlags & ImGuiNavFlags_NoCaptureKeyboard)) + g.IO.WantCaptureKeyboard = true; + + g.IO.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : 0; + g.MouseCursor = ImGuiMouseCursor_Arrow; + g.WantCaptureMouseNextFrame = g.WantCaptureKeyboardNextFrame = g.WantTextInputNextFrame = -1; + g.OsImePosRequest = ImVec2(1.0f, 1.0f); // OS Input Method Editor showing on top-left of our window by default + + // If mouse was first clicked outside of ImGui bounds we also cancel out hovering. + // FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02) + bool mouse_dragging_extern_payload = g.DragDropActive && (g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) != 0; + if (!mouse_avail_to_imgui && !mouse_dragging_extern_payload) + g.HoveredWindow = g.HoveredRootWindow = NULL; + + // Mouse wheel scrolling, scale + if (g.HoveredWindow && !g.HoveredWindow->Collapsed && (g.IO.MouseWheel != 0.0f || g.IO.MouseWheelH != 0.0f)) + { + // If a child window has the ImGuiWindowFlags_NoScrollWithMouse flag, we give a chance to scroll its parent (unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set). + ImGuiWindow* window = g.HoveredWindow; + ImGuiWindow* scroll_window = window; + while ((scroll_window->Flags & ImGuiWindowFlags_ChildWindow) && (scroll_window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(scroll_window->Flags & ImGuiWindowFlags_NoScrollbar) && !(scroll_window->Flags & ImGuiWindowFlags_NoInputs) && scroll_window->ParentWindow) + scroll_window = scroll_window->ParentWindow; + const bool scroll_allowed = !(scroll_window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(scroll_window->Flags & ImGuiWindowFlags_NoInputs); + + if (g.IO.MouseWheel != 0.0f) + { + if (g.IO.KeyCtrl && g.IO.FontAllowUserScaling) + { + // Zoom / Scale window + const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); + const float scale = new_font_scale / window->FontWindowScale; + window->FontWindowScale = new_font_scale; + + const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; + window->Pos += offset; + window->PosFloat += offset; + window->Size *= scale; + window->SizeFull *= scale; + } + else if (!g.IO.KeyCtrl && scroll_allowed) + { + // Mouse wheel vertical scrolling + float scroll_amount = 5 * scroll_window->CalcFontSize(); + scroll_amount = (float)(int)ImMin(scroll_amount, (scroll_window->ContentsRegionRect.GetHeight() + scroll_window->WindowPadding.y * 2.0f) * 0.67f); + SetWindowScrollY(scroll_window, scroll_window->Scroll.y - g.IO.MouseWheel * scroll_amount); + } + } + if (g.IO.MouseWheelH != 0.0f && scroll_allowed) + { + // Mouse wheel horizontal scrolling (for hardware that supports it) + float scroll_amount = scroll_window->CalcFontSize(); + if (!g.IO.KeyCtrl && !(window->Flags & ImGuiWindowFlags_NoScrollWithMouse)) + SetWindowScrollX(window, window->Scroll.x - g.IO.MouseWheelH * scroll_amount); + } + } + + // Pressing TAB activate widget focus + if (g.ActiveId == 0 && g.NavWindow != NULL && g.NavWindow->Active && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab, false)) + { + if (g.NavId != 0 && g.NavIdTabCounter != INT_MAX) + g.NavWindow->FocusIdxTabRequestNext = g.NavIdTabCounter + 1 + (g.IO.KeyShift ? -1 : 1); + else + g.NavWindow->FocusIdxTabRequestNext = g.IO.KeyShift ? -1 : 0; + } + g.NavIdTabCounter = INT_MAX; + + // Mark all windows as not visible + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* window = g.Windows[i]; + window->WasActive = window->Active; + window->Active = false; + window->WriteAccessed = false; + } + + // Closing the focused window restore focus to the first active root window in descending z-order + if (g.NavWindow && !g.NavWindow->WasActive) + FocusFrontMostActiveWindow(NULL); + + // No window should be open at the beginning of the frame. + // But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear. + g.CurrentWindowStack.resize(0); + g.CurrentPopupStack.resize(0); + ClosePopupsOverWindow(g.NavWindow); + + // Create implicit window - we will only render it if the user has added something to it. + // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. + SetNextWindowSize(ImVec2(400,400), ImGuiCond_FirstUseEver); + Begin("Debug##Default"); +} + +static void* SettingsHandlerWindow_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiWindowSettings* settings = ImGui::FindWindowSettings(ImHash(name, 0)); + if (!settings) + settings = AddWindowSettings(name); + return (void*)settings; +} + +static void SettingsHandlerWindow_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + ImGuiWindowSettings* settings = (ImGuiWindowSettings*)entry; + float x, y; + int i; + if (sscanf(line, "Pos=%f,%f", &x, &y) == 2) settings->Pos = ImVec2(x, y); + else if (sscanf(line, "Size=%f,%f", &x, &y) == 2) settings->Size = ImMax(ImVec2(x, y), GImGui->Style.WindowMinSize); + else if (sscanf(line, "Collapsed=%d", &i) == 1) settings->Collapsed = (i != 0); +} + +static void SettingsHandlerWindow_WriteAll(ImGuiContext* imgui_ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + // Gather data from windows that were active during this session + ImGuiContext& g = *imgui_ctx; + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Flags & ImGuiWindowFlags_NoSavedSettings) + continue; + ImGuiWindowSettings* settings = ImGui::FindWindowSettings(window->ID); + if (!settings) + settings = AddWindowSettings(window->Name); + settings->Pos = window->Pos; + settings->Size = window->SizeFull; + settings->Collapsed = window->Collapsed; + } + + // Write a buffer + // If a window wasn't opened in this session we preserve its settings + buf->reserve(buf->size() + g.SettingsWindows.Size * 96); // ballpark reserve + for (int i = 0; i != g.SettingsWindows.Size; i++) + { + const ImGuiWindowSettings* settings = &g.SettingsWindows[i]; + if (settings->Pos.x == FLT_MAX) + continue; + const char* name = settings->Name; + if (const char* p = strstr(name, "###")) // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() + name = p; + buf->appendf("[%s][%s]\n", handler->TypeName, name); + buf->appendf("Pos=%d,%d\n", (int)settings->Pos.x, (int)settings->Pos.y); + buf->appendf("Size=%d,%d\n", (int)settings->Size.x, (int)settings->Size.y); + buf->appendf("Collapsed=%d\n", settings->Collapsed); + buf->appendf("\n"); + } +} + +void ImGui::Initialize(ImGuiContext* context) +{ + ImGuiContext& g = *context; + IM_ASSERT(!g.Initialized && !g.SettingsLoaded); + g.LogClipboard = IM_NEW(ImGuiTextBuffer)(); + + // Add .ini handle for ImGuiWindow type + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Window"; + ini_handler.TypeHash = ImHash("Window", 0, 0); + ini_handler.ReadOpenFn = SettingsHandlerWindow_ReadOpen; + ini_handler.ReadLineFn = SettingsHandlerWindow_ReadLine; + ini_handler.WriteAllFn = SettingsHandlerWindow_WriteAll; + g.SettingsHandlers.push_front(ini_handler); + + g.Initialized = true; +} + +// This function is merely here to free heap allocations. +void ImGui::Shutdown(ImGuiContext* context) +{ + ImGuiContext& g = *context; + + // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) + if (g.IO.Fonts && g.FontAtlasOwnedByContext) + IM_DELETE(g.IO.Fonts); + + // Cleanup of other data are conditional on actually having initialize ImGui. + if (!g.Initialized) + return; + + SaveIniSettingsToDisk(g.IO.IniFilename); + + // Clear everything else + for (int i = 0; i < g.Windows.Size; i++) + IM_DELETE(g.Windows[i]); + g.Windows.clear(); + g.WindowsSortBuffer.clear(); + g.CurrentWindow = NULL; + g.CurrentWindowStack.clear(); + g.WindowsById.Clear(); + g.NavWindow = NULL; + g.HoveredWindow = NULL; + g.HoveredRootWindow = NULL; + g.ActiveIdWindow = NULL; + g.MovingWindow = NULL; + for (int i = 0; i < g.SettingsWindows.Size; i++) + IM_DELETE(g.SettingsWindows[i].Name); + g.ColorModifiers.clear(); + g.StyleModifiers.clear(); + g.FontStack.clear(); + g.OpenPopupStack.clear(); + g.CurrentPopupStack.clear(); + g.DrawDataBuilder.ClearFreeMemory(); + g.OverlayDrawList.ClearFreeMemory(); + g.PrivateClipboard.clear(); + g.InputTextState.Text.clear(); + g.InputTextState.InitialText.clear(); + g.InputTextState.TempTextBuffer.clear(); + + g.SettingsWindows.clear(); + g.SettingsHandlers.clear(); + + if (g.LogFile && g.LogFile != stdout) + { + fclose(g.LogFile); + g.LogFile = NULL; + } + if (g.LogClipboard) + IM_DELETE(g.LogClipboard); + + g.Initialized = false; +} + +ImGuiWindowSettings* ImGui::FindWindowSettings(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (int i = 0; i != g.SettingsWindows.Size; i++) + if (g.SettingsWindows[i].Id == id) + return &g.SettingsWindows[i]; + return NULL; +} + +static ImGuiWindowSettings* AddWindowSettings(const char* name) +{ + ImGuiContext& g = *GImGui; + g.SettingsWindows.push_back(ImGuiWindowSettings()); + ImGuiWindowSettings* settings = &g.SettingsWindows.back(); + settings->Name = ImStrdup(name); + settings->Id = ImHash(name, 0); + return settings; +} + +static void LoadIniSettingsFromDisk(const char* ini_filename) +{ + if (!ini_filename) + return; + char* file_data = (char*)ImFileLoadToMemory(ini_filename, "rb", NULL, +1); + if (!file_data) + return; + LoadIniSettingsFromMemory(file_data); + ImGui::MemFree(file_data); +} + +ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + const ImGuiID type_hash = ImHash(type_name, 0, 0); + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + if (g.SettingsHandlers[handler_n].TypeHash == type_hash) + return &g.SettingsHandlers[handler_n]; + return NULL; +} + +// Zero-tolerance, no error reporting, cheap .ini parsing +static void LoadIniSettingsFromMemory(const char* buf_readonly) +{ + // For convenience and to make the code simpler, we'll write zero terminators inside the buffer. So let's create a writable copy. + char* buf = ImStrdup(buf_readonly); + char* buf_end = buf + strlen(buf); + + ImGuiContext& g = *GImGui; + void* entry_data = NULL; + ImGuiSettingsHandler* entry_handler = NULL; + + char* line_end = NULL; + for (char* line = buf; line < buf_end; line = line_end + 1) + { + // Skip new lines markers, then find end of the line + while (*line == '\n' || *line == '\r') + line++; + line_end = line; + while (line_end < buf_end && *line_end != '\n' && *line_end != '\r') + line_end++; + line_end[0] = 0; + + if (line[0] == '[' && line_end > line && line_end[-1] == ']') + { + // Parse "[Type][Name]". Note that 'Name' can itself contains [] characters, which is acceptable with the current format and parsing code. + line_end[-1] = 0; + const char* name_end = line_end - 1; + const char* type_start = line + 1; + char* type_end = ImStrchrRange(type_start, name_end, ']'); + const char* name_start = type_end ? ImStrchrRange(type_end + 1, name_end, '[') : NULL; + if (!type_end || !name_start) + { + name_start = type_start; // Import legacy entries that have no type + type_start = "Window"; + } + else + { + *type_end = 0; // Overwrite first ']' + name_start++; // Skip second '[' + } + entry_handler = ImGui::FindSettingsHandler(type_start); + entry_data = entry_handler ? entry_handler->ReadOpenFn(&g, entry_handler, name_start) : NULL; + } + else if (entry_handler != NULL && entry_data != NULL) + { + // Let type handler parse the line + entry_handler->ReadLineFn(&g, entry_handler, entry_data, line); + } + } + ImGui::MemFree(buf); + g.SettingsLoaded = true; +} + +static void SaveIniSettingsToDisk(const char* ini_filename) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + if (!ini_filename) + return; + + ImVector buf; + SaveIniSettingsToMemory(buf); + + FILE* f = ImFileOpen(ini_filename, "wt"); + if (!f) + return; + fwrite(buf.Data, sizeof(char), (size_t)buf.Size, f); + fclose(f); +} + +static void SaveIniSettingsToMemory(ImVector& out_buf) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + + ImGuiTextBuffer buf; + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + { + ImGuiSettingsHandler* handler = &g.SettingsHandlers[handler_n]; + handler->WriteAllFn(&g, handler, &buf); + } + + buf.Buf.pop_back(); // Remove extra zero-terminator used by ImGuiTextBuffer + out_buf.swap(buf.Buf); +} + +void ImGui::MarkIniSettingsDirty() +{ + ImGuiContext& g = *GImGui; + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +static void MarkIniSettingsDirty(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +// FIXME: Add a more explicit sort order in the window structure. +static int IMGUI_CDECL ChildWindowComparer(const void* lhs, const void* rhs) +{ + const ImGuiWindow* a = *(const ImGuiWindow**)lhs; + const ImGuiWindow* b = *(const ImGuiWindow**)rhs; + if (int d = (a->Flags & ImGuiWindowFlags_Popup) - (b->Flags & ImGuiWindowFlags_Popup)) + return d; + if (int d = (a->Flags & ImGuiWindowFlags_Tooltip) - (b->Flags & ImGuiWindowFlags_Tooltip)) + return d; + return (a->BeginOrderWithinParent - b->BeginOrderWithinParent); +} + +static void AddWindowToSortedBuffer(ImVector* out_sorted_windows, ImGuiWindow* window) +{ + out_sorted_windows->push_back(window); + if (window->Active) + { + int count = window->DC.ChildWindows.Size; + if (count > 1) + qsort(window->DC.ChildWindows.begin(), (size_t)count, sizeof(ImGuiWindow*), ChildWindowComparer); + for (int i = 0; i < count; i++) + { + ImGuiWindow* child = window->DC.ChildWindows[i]; + if (child->Active) + AddWindowToSortedBuffer(out_sorted_windows, child); + } + } +} + +static void AddDrawListToDrawData(ImVector* out_render_list, ImDrawList* draw_list) +{ + if (draw_list->CmdBuffer.empty()) + return; + + // Remove trailing command if unused + ImDrawCmd& last_cmd = draw_list->CmdBuffer.back(); + if (last_cmd.ElemCount == 0 && last_cmd.UserCallback == NULL) + { + draw_list->CmdBuffer.pop_back(); + if (draw_list->CmdBuffer.empty()) + return; + } + + // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. May trigger for you if you are using PrimXXX functions incorrectly. + IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); + IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); + IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); + + // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) + // If this assert triggers because you are drawing lots of stuff manually: + // A) Make sure you are coarse clipping, because ImDrawList let all your vertices pass. You can use the Metrics window to inspect draw list contents. + // B) If you need/want meshes with more than 64K vertices, uncomment the '#define ImDrawIdx unsigned int' line in imconfig.h to set the index size to 4 bytes. + // You'll need to handle the 4-bytes indices to your renderer. For example, the OpenGL example code detect index size at compile-time by doing: + // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); + // Your own engine or render API may use different parameters or function calls to specify index sizes. 2 and 4 bytes indices are generally supported by most API. + // C) If for some reason you cannot use 4 bytes indices or don't want to, a workaround is to call BeginChild()/EndChild() before reaching the 64K limit to split your draw commands in multiple draw lists. + if (sizeof(ImDrawIdx) == 2) + IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); + + out_render_list->push_back(draw_list); +} + +static void AddWindowToDrawData(ImVector* out_render_list, ImGuiWindow* window) +{ + AddDrawListToDrawData(out_render_list, window->DrawList); + for (int i = 0; i < window->DC.ChildWindows.Size; i++) + { + ImGuiWindow* child = window->DC.ChildWindows[i]; + if (child->Active && child->HiddenFrames <= 0) // clipped children may have been marked not active + AddWindowToDrawData(out_render_list, child); + } +} + +static void AddWindowToDrawDataSelectLayer(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.IO.MetricsActiveWindows++; + if (window->Flags & ImGuiWindowFlags_Tooltip) + AddWindowToDrawData(&g.DrawDataBuilder.Layers[1], window); + else + AddWindowToDrawData(&g.DrawDataBuilder.Layers[0], window); +} + +void ImDrawDataBuilder::FlattenIntoSingleLayer() +{ + int n = Layers[0].Size; + int size = n; + for (int i = 1; i < IM_ARRAYSIZE(Layers); i++) + size += Layers[i].Size; + Layers[0].resize(size); + for (int layer_n = 1; layer_n < IM_ARRAYSIZE(Layers); layer_n++) + { + ImVector& layer = Layers[layer_n]; + if (layer.empty()) + continue; + memcpy(&Layers[0][n], &layer[0], layer.Size * sizeof(ImDrawList*)); + n += layer.Size; + layer.resize(0); + } +} + +static void SetupDrawData(ImVector* draw_lists, ImDrawData* out_draw_data) +{ + out_draw_data->Valid = true; + out_draw_data->CmdLists = (draw_lists->Size > 0) ? draw_lists->Data : NULL; + out_draw_data->CmdListsCount = draw_lists->Size; + out_draw_data->TotalVtxCount = out_draw_data->TotalIdxCount = 0; + for (int n = 0; n < draw_lists->Size; n++) + { + out_draw_data->TotalVtxCount += draw_lists->Data[n]->VtxBuffer.Size; + out_draw_data->TotalIdxCount += draw_lists->Data[n]->IdxBuffer.Size; + } +} + +// When using this function it is sane to ensure that float are perfectly rounded to integer values, to that e.g. (int)(max.x-min.x) in user's render produce correct result. +void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +void ImGui::PopClipRect() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PopClipRect(); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +// This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. +void ImGui::EndFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() + if (g.FrameCountEnded == g.FrameCount) // Don't process EndFrame() multiple times. + return; + + // Notify OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) + if (g.IO.ImeSetInputScreenPosFn && ImLengthSqr(g.OsImePosRequest - g.OsImePosSet) > 0.0001f) + { + g.IO.ImeSetInputScreenPosFn((int)g.OsImePosRequest.x, (int)g.OsImePosRequest.y); + g.OsImePosSet = g.OsImePosRequest; + } + + // Hide implicit "Debug" window if it hasn't been used + IM_ASSERT(g.CurrentWindowStack.Size == 1); // Mismatched Begin()/End() calls + if (g.CurrentWindow && !g.CurrentWindow->WriteAccessed) + g.CurrentWindow->Active = false; + End(); + + if (g.ActiveId == 0 && g.HoveredId == 0) + { + if (!g.NavWindow || !g.NavWindow->Appearing) // Unless we just made a window/popup appear + { + // Click to focus window and start moving (after we're done with all our widgets) + if (g.IO.MouseClicked[0]) + { + if (g.HoveredRootWindow != NULL) + { + // Set ActiveId even if the _NoMove flag is set, without it dragging away from a window with _NoMove would activate hover on other windows. + FocusWindow(g.HoveredWindow); + SetActiveID(g.HoveredWindow->MoveId, g.HoveredWindow); + g.NavDisableHighlight = true; + g.ActiveIdClickOffset = g.IO.MousePos - g.HoveredRootWindow->Pos; + if (!(g.HoveredWindow->Flags & ImGuiWindowFlags_NoMove) && !(g.HoveredRootWindow->Flags & ImGuiWindowFlags_NoMove)) + g.MovingWindow = g.HoveredWindow; + } + else if (g.NavWindow != NULL && GetFrontMostModalRootWindow() == NULL) + { + // Clicking on void disable focus + FocusWindow(NULL); + } + } + + // With right mouse button we close popups without changing focus + // (The left mouse button path calls FocusWindow which will lead NewFrame->ClosePopupsOverWindow to trigger) + if (g.IO.MouseClicked[1]) + { + // Find the top-most window between HoveredWindow and the front most Modal Window. + // This is where we can trim the popup stack. + ImGuiWindow* modal = GetFrontMostModalRootWindow(); + bool hovered_window_above_modal = false; + if (modal == NULL) + hovered_window_above_modal = true; + for (int i = g.Windows.Size - 1; i >= 0 && hovered_window_above_modal == false; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (window == modal) + break; + if (window == g.HoveredWindow) + hovered_window_above_modal = true; + } + ClosePopupsOverWindow(hovered_window_above_modal ? g.HoveredWindow : modal); + } + } + } + + // Sort the window list so that all child windows are after their parent + // We cannot do that on FocusWindow() because childs may not exist yet + g.WindowsSortBuffer.resize(0); + g.WindowsSortBuffer.reserve(g.Windows.Size); + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it + continue; + AddWindowToSortedBuffer(&g.WindowsSortBuffer, window); + } + + IM_ASSERT(g.Windows.Size == g.WindowsSortBuffer.Size); // we done something wrong + g.Windows.swap(g.WindowsSortBuffer); + + // Clear Input data for next frame + g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; + memset(g.IO.InputCharacters, 0, sizeof(g.IO.InputCharacters)); + memset(g.IO.NavInputs, 0, sizeof(g.IO.NavInputs)); + + g.FrameCountEnded = g.FrameCount; +} + +void ImGui::Render() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() + + if (g.FrameCountEnded != g.FrameCount) + ImGui::EndFrame(); + g.FrameCountRendered = g.FrameCount; + + // Skip render altogether if alpha is 0.0 + // Note that vertex buffers have been created and are wasted, so it is best practice that you don't create windows in the first place, or consistently respond to Begin() returning false. + if (g.Style.Alpha > 0.0f) + { + // Gather windows to render + g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = g.IO.MetricsActiveWindows = 0; + g.DrawDataBuilder.Clear(); + ImGuiWindow* window_to_render_front_most = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget : NULL; + for (int n = 0; n != g.Windows.Size; n++) + { + ImGuiWindow* window = g.Windows[n]; + if (window->Active && window->HiddenFrames <= 0 && (window->Flags & (ImGuiWindowFlags_ChildWindow)) == 0 && window != window_to_render_front_most) + AddWindowToDrawDataSelectLayer(window); + } + if (window_to_render_front_most && window_to_render_front_most->Active && window_to_render_front_most->HiddenFrames <= 0) // NavWindowingTarget is always temporarily displayed as the front-most window + AddWindowToDrawDataSelectLayer(window_to_render_front_most); + g.DrawDataBuilder.FlattenIntoSingleLayer(); + + // Draw software mouse cursor if requested + ImVec2 offset, size, uv[4]; + if (g.IO.MouseDrawCursor && g.IO.Fonts->GetMouseCursorTexData(g.MouseCursor, &offset, &size, &uv[0], &uv[2])) + { + const ImVec2 pos = g.IO.MousePos - offset; + const ImTextureID tex_id = g.IO.Fonts->TexID; + const float sc = g.Style.MouseCursorScale; + g.OverlayDrawList.PushTextureID(tex_id); + g.OverlayDrawList.AddImage(tex_id, pos + ImVec2(1,0)*sc, pos+ImVec2(1,0)*sc + size*sc, uv[2], uv[3], IM_COL32(0,0,0,48)); // Shadow + g.OverlayDrawList.AddImage(tex_id, pos + ImVec2(2,0)*sc, pos+ImVec2(2,0)*sc + size*sc, uv[2], uv[3], IM_COL32(0,0,0,48)); // Shadow + g.OverlayDrawList.AddImage(tex_id, pos, pos + size*sc, uv[2], uv[3], IM_COL32(0,0,0,255)); // Black border + g.OverlayDrawList.AddImage(tex_id, pos, pos + size*sc, uv[0], uv[1], IM_COL32(255,255,255,255)); // White fill + g.OverlayDrawList.PopTextureID(); + } + if (!g.OverlayDrawList.VtxBuffer.empty()) + AddDrawListToDrawData(&g.DrawDataBuilder.Layers[0], &g.OverlayDrawList); + + // Setup ImDrawData structure for end-user + SetupDrawData(&g.DrawDataBuilder.Layers[0], &g.DrawData); + g.IO.MetricsRenderVertices = g.DrawData.TotalVtxCount; + g.IO.MetricsRenderIndices = g.DrawData.TotalIdxCount; + + // Render. If user hasn't set a callback then they may retrieve the draw data via GetDrawData() +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (g.DrawData.CmdListsCount > 0 && g.IO.RenderDrawListsFn != NULL) + g.IO.RenderDrawListsFn(&g.DrawData); +#endif + } +} + +const char* ImGui::FindRenderedTextEnd(const char* text, const char* text_end) +{ + const char* text_display_end = text; + if (!text_end) + text_end = (const char*)-1; + + while (text_display_end < text_end && *text_display_end != '\0' && (text_display_end[0] != '#' || text_display_end[1] != '#')) + text_display_end++; + return text_display_end; +} + +// Pass text data straight to log (without being displayed) +void ImGui::LogText(const char* fmt, ...) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + va_list args; + va_start(args, fmt); + if (g.LogFile) + { + vfprintf(g.LogFile, fmt, args); + } + else + { + g.LogClipboard->appendfv(fmt, args); + } + va_end(args); +} + +// Internal version that takes a position to decide on newline placement and pad items according to their depth. +// We split text into individual lines to add current tree level padding +static void LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end = NULL) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (!text_end) + text_end = ImGui::FindRenderedTextEnd(text, text_end); + + const bool log_new_line = ref_pos && (ref_pos->y > window->DC.LogLinePosY + 1); + if (ref_pos) + window->DC.LogLinePosY = ref_pos->y; + + const char* text_remaining = text; + if (g.LogStartDepth > window->DC.TreeDepth) // Re-adjust padding if we have popped out of our starting depth + g.LogStartDepth = window->DC.TreeDepth; + const int tree_depth = (window->DC.TreeDepth - g.LogStartDepth); + for (;;) + { + // Split the string. Each new line (after a '\n') is followed by spacing corresponding to the current depth of our log entry. + const char* line_end = text_remaining; + while (line_end < text_end) + if (*line_end == '\n') + break; + else + line_end++; + if (line_end >= text_end) + line_end = NULL; + + const bool is_first_line = (text == text_remaining); + bool is_last_line = false; + if (line_end == NULL) + { + is_last_line = true; + line_end = text_end; + } + if (line_end != NULL && !(is_last_line && (line_end - text_remaining)==0)) + { + const int char_count = (int)(line_end - text_remaining); + if (log_new_line || !is_first_line) + ImGui::LogText(IM_NEWLINE "%*s%.*s", tree_depth*4, "", char_count, text_remaining); + else + ImGui::LogText(" %.*s", char_count, text_remaining); + } + + if (is_last_line) + break; + text_remaining = line_end + 1; + } +} + +// Internal ImGui functions to render text +// RenderText***() functions calls ImDrawList::AddText() calls ImBitmapFont::RenderText() +void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Hide anything after a '##' string + const char* text_display_end; + if (hide_text_after_hash) + { + text_display_end = FindRenderedTextEnd(text, text_end); + } + else + { + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT + text_display_end = text_end; + } + + const int text_len = (int)(text_display_end - text); + if (text_len > 0) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_display_end); + } +} + +void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT + + const int text_len = (int)(text_end - text); + if (text_len > 0) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_end, wrap_width); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_end); + } +} + +// Default clip_rect uses (pos_min,pos_max) +// Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges) +void ImGui::RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Hide anything after a '##' string + const char* text_display_end = FindRenderedTextEnd(text, text_end); + const int text_len = (int)(text_display_end - text); + if (text_len == 0) + return; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Perform CPU side clipping for single clipped element to avoid using scissor state + ImVec2 pos = pos_min; + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_display_end, false, 0.0f); + + const ImVec2* clip_min = clip_rect ? &clip_rect->Min : &pos_min; + const ImVec2* clip_max = clip_rect ? &clip_rect->Max : &pos_max; + bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y); + if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min + need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y); + + // Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment. + if (align.x > 0.0f) pos.x = ImMax(pos.x, pos.x + (pos_max.x - pos.x - text_size.x) * align.x); + if (align.y > 0.0f) pos.y = ImMax(pos.y, pos.y + (pos_max.y - pos.y - text_size.y) * align.y); + + // Render + if (need_clipping) + { + ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y); + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect); + } + else + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, NULL); + } + if (g.LogEnabled) + LogRenderedText(&pos, text, text_display_end); +} + +// Render a rectangle shaped with optional rounding and borders +void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border, float rounding) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); + const float border_size = g.Style.FrameBorderSize; + if (border && border_size > 0.0f) + { + window->DrawList->AddRect(p_min+ImVec2(1,1), p_max+ImVec2(1,1), GetColorU32(ImGuiCol_BorderShadow), rounding, ImDrawCornerFlags_All, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); + } +} + +void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const float border_size = g.Style.FrameBorderSize; + if (border_size > 0.0f) + { + window->DrawList->AddRect(p_min+ImVec2(1,1), p_max+ImVec2(1,1), GetColorU32(ImGuiCol_BorderShadow), rounding, ImDrawCornerFlags_All, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); + } +} + +// Render a triangle to denote expanded/collapsed state +void ImGui::RenderTriangle(ImVec2 p_min, ImGuiDir dir, float scale) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const float h = g.FontSize * 1.00f; + float r = h * 0.40f * scale; + ImVec2 center = p_min + ImVec2(h * 0.50f, h * 0.50f * scale); + + ImVec2 a, b, c; + switch (dir) + { + case ImGuiDir_Up: + case ImGuiDir_Down: + if (dir == ImGuiDir_Up) r = -r; + center.y -= r * 0.25f; + a = ImVec2(0,1) * r; + b = ImVec2(-0.866f,-0.5f) * r; + c = ImVec2(+0.866f,-0.5f) * r; + break; + case ImGuiDir_Left: + case ImGuiDir_Right: + if (dir == ImGuiDir_Left) r = -r; + center.x -= r * 0.25f; + a = ImVec2(1,0) * r; + b = ImVec2(-0.500f,+0.866f) * r; + c = ImVec2(-0.500f,-0.866f) * r; + break; + case ImGuiDir_None: + case ImGuiDir_Count_: + IM_ASSERT(0); + break; + } + + window->DrawList->AddTriangleFilled(center + a, center + b, center + c, GetColorU32(ImGuiCol_Text)); +} + +void ImGui::RenderBullet(ImVec2 pos) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddCircleFilled(pos, GImGui->FontSize*0.20f, GetColorU32(ImGuiCol_Text), 8); +} + +void ImGui::RenderCheckMark(ImVec2 pos, ImU32 col, float sz) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + float thickness = ImMax(sz / 5.0f, 1.0f); + sz -= thickness*0.5f; + pos += ImVec2(thickness*0.25f, thickness*0.25f); + + float third = sz / 3.0f; + float bx = pos.x + third; + float by = pos.y + sz - third*0.5f; + window->DrawList->PathLineTo(ImVec2(bx - third, by - third)); + window->DrawList->PathLineTo(ImVec2(bx, by)); + window->DrawList->PathLineTo(ImVec2(bx + third*2, by - third*2)); + window->DrawList->PathStroke(col, false, thickness); +} + +void ImGui::RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags) +{ + ImGuiContext& g = *GImGui; + if (id != g.NavId) + return; + if (g.NavDisableHighlight && !(flags & ImGuiNavHighlightFlags_AlwaysDraw)) + return; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->DC.NavHideHighlightOneFrame) + return; + + float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; + ImRect display_rect = bb; + display_rect.ClipWith(window->ClipRect); + if (flags & ImGuiNavHighlightFlags_TypeDefault) + { + const float THICKNESS = 2.0f; + const float DISTANCE = 3.0f + THICKNESS * 0.5f; + display_rect.Expand(ImVec2(DISTANCE,DISTANCE)); + bool fully_visible = window->ClipRect.Contains(display_rect); + if (!fully_visible) + window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); + window->DrawList->AddRect(display_rect.Min + ImVec2(THICKNESS*0.5f,THICKNESS*0.5f), display_rect.Max - ImVec2(THICKNESS*0.5f,THICKNESS*0.5f), GetColorU32(ImGuiCol_NavHighlight), rounding, ImDrawCornerFlags_All, THICKNESS); + if (!fully_visible) + window->DrawList->PopClipRect(); + } + if (flags & ImGuiNavHighlightFlags_TypeThin) + { + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, ~0, 1.0f); + } +} + +// Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. +// CalcTextSize("") should return ImVec2(0.0f, GImGui->FontSize) +ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) +{ + ImGuiContext& g = *GImGui; + + const char* text_display_end; + if (hide_text_after_double_hash) + text_display_end = FindRenderedTextEnd(text, text_end); // Hide anything after a '##' string + else + text_display_end = text_end; + + ImFont* font = g.Font; + const float font_size = g.FontSize; + if (text == text_display_end) + return ImVec2(0.0f, font_size); + ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); + + // Cancel out character spacing for the last character of a line (it is baked into glyph->AdvanceX field) + const float font_scale = font_size / font->FontSize; + const float character_spacing_x = 1.0f * font_scale; + if (text_size.x > 0.0f) + text_size.x -= character_spacing_x; + text_size.x = (float)(int)(text_size.x + 0.95f); + + return text_size; +} + +// Helper to calculate coarse clipping of large list of evenly sized items. +// NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern. +// NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX +void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + *out_items_display_start = 0; + *out_items_display_end = items_count; + return; + } + if (window->SkipItems) + { + *out_items_display_start = *out_items_display_end = 0; + return; + } + + const ImVec2 pos = window->DC.CursorPos; + int start = (int)((window->ClipRect.Min.y - pos.y) / items_height); + int end = (int)((window->ClipRect.Max.y - pos.y) / items_height); + if (g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Up) // When performing a navigation request, ensure we have one item extra in the direction we are moving to + start--; + if (g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) + end++; + + start = ImClamp(start, 0, items_count); + end = ImClamp(end + 1, start, items_count); + *out_items_display_start = start; + *out_items_display_end = end; +} + +// Find window given position, search front-to-back +// FIXME: Note that we have a lag here because WindowRectClipped is updated in Begin() so windows moved by user via SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is called, aka before the next Begin(). Moving window thankfully isn't affected. +static ImGuiWindow* FindHoveredWindow() +{ + ImGuiContext& g = *GImGui; + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (!window->Active) + continue; + if (window->Flags & ImGuiWindowFlags_NoInputs) + continue; + + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImRect bb(window->WindowRectClipped.Min - g.Style.TouchExtraPadding, window->WindowRectClipped.Max + g.Style.TouchExtraPadding); + if (bb.Contains(g.IO.MousePos)) + return window; + } + return NULL; +} + +// Test if mouse cursor is hovering given rectangle +// NB- Rectangle is clipped by our current clip setting +// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) +bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Clip + ImRect rect_clipped(r_min, r_max); + if (clip) + rect_clipped.ClipWith(window->ClipRect); + + // Expand for touch input + const ImRect rect_for_touch(rect_clipped.Min - g.Style.TouchExtraPadding, rect_clipped.Max + g.Style.TouchExtraPadding); + return rect_for_touch.Contains(g.IO.MousePos); +} + +static bool IsKeyPressedMap(ImGuiKey key, bool repeat) +{ + const int key_index = GImGui->IO.KeyMap[key]; + return (key_index >= 0) ? ImGui::IsKeyPressed(key_index, repeat) : false; +} + +int ImGui::GetKeyIndex(ImGuiKey imgui_key) +{ + IM_ASSERT(imgui_key >= 0 && imgui_key < ImGuiKey_COUNT); + return GImGui->IO.KeyMap[imgui_key]; +} + +// Note that imgui doesn't know the semantic of each entry of io.KeyDown[]. Use your own indices/enums according to how your back-end/engine stored them into KeyDown[]! +bool ImGui::IsKeyDown(int user_key_index) +{ + if (user_key_index < 0) return false; + IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(GImGui->IO.KeysDown)); + return GImGui->IO.KeysDown[user_key_index]; +} + +int ImGui::CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate) +{ + if (t == 0.0f) + return 1; + if (t <= repeat_delay || repeat_rate <= 0.0f) + return 0; + const int count = (int)((t - repeat_delay) / repeat_rate) - (int)((t_prev - repeat_delay) / repeat_rate); + return (count > 0) ? count : 0; +} + +int ImGui::GetKeyPressedAmount(int key_index, float repeat_delay, float repeat_rate) +{ + ImGuiContext& g = *GImGui; + if (key_index < 0) return false; + IM_ASSERT(key_index >= 0 && key_index < IM_ARRAYSIZE(g.IO.KeysDown)); + const float t = g.IO.KeysDownDuration[key_index]; + return CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, repeat_delay, repeat_rate); +} + +bool ImGui::IsKeyPressed(int user_key_index, bool repeat) +{ + ImGuiContext& g = *GImGui; + if (user_key_index < 0) return false; + IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); + const float t = g.IO.KeysDownDuration[user_key_index]; + if (t == 0.0f) + return true; + if (repeat && t > g.IO.KeyRepeatDelay) + return GetKeyPressedAmount(user_key_index, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; + return false; +} + +bool ImGui::IsKeyReleased(int user_key_index) +{ + ImGuiContext& g = *GImGui; + if (user_key_index < 0) return false; + IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); + return g.IO.KeysDownDurationPrev[user_key_index] >= 0.0f && !g.IO.KeysDown[user_key_index]; +} + +bool ImGui::IsMouseDown(int button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button]; +} + +bool ImGui::IsAnyMouseDown() +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) + if (g.IO.MouseDown[n]) + return true; + return false; +} + +bool ImGui::IsMouseClicked(int button, bool repeat) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + const float t = g.IO.MouseDownDuration[button]; + if (t == 0.0f) + return true; + + if (repeat && t > g.IO.KeyRepeatDelay) + { + float delay = g.IO.KeyRepeatDelay, rate = g.IO.KeyRepeatRate; + if ((fmodf(t - delay, rate) > rate*0.5f) != (fmodf(t - delay - g.IO.DeltaTime, rate) > rate*0.5f)) + return true; + } + + return false; +} + +bool ImGui::IsMouseReleased(int button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button]; +} + +bool ImGui::IsMouseDoubleClicked(int button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDoubleClicked[button]; +} + +bool ImGui::IsMouseDragging(int button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) + return false; + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; +} + +ImVec2 ImGui::GetMousePos() +{ + return GImGui->IO.MousePos; +} + +// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! +ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() +{ + ImGuiContext& g = *GImGui; + if (g.CurrentPopupStack.Size > 0) + return g.OpenPopupStack[g.CurrentPopupStack.Size-1].OpenMousePos; + return g.IO.MousePos; +} + +// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position +bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) +{ + if (mouse_pos == NULL) + mouse_pos = &GImGui->IO.MousePos; + const float MOUSE_INVALID = -256000.0f; + return mouse_pos->x >= MOUSE_INVALID && mouse_pos->y >= MOUSE_INVALID; +} + +// NB: This is only valid if IsMousePosValid(). Back-ends in theory should always keep mouse position valid when dragging even outside the client window. +ImVec2 ImGui::GetMouseDragDelta(int button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + if (g.IO.MouseDown[button]) + if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) + return g.IO.MousePos - g.IO.MouseClickedPos[button]; // Assume we can only get active with left-mouse button (at the moment). + return ImVec2(0.0f, 0.0f); +} + +void ImGui::ResetMouseDragDelta(int button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr + g.IO.MouseClickedPos[button] = g.IO.MousePos; +} + +ImGuiMouseCursor ImGui::GetMouseCursor() +{ + return GImGui->MouseCursor; +} + +void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) +{ + GImGui->MouseCursor = cursor_type; +} + +void ImGui::CaptureKeyboardFromApp(bool capture) +{ + GImGui->WantCaptureKeyboardNextFrame = capture ? 1 : 0; +} + +void ImGui::CaptureMouseFromApp(bool capture) +{ + GImGui->WantCaptureMouseNextFrame = capture ? 1 : 0; +} + +bool ImGui::IsItemActive() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId) + { + ImGuiWindow* window = g.CurrentWindow; + return g.ActiveId == window->DC.LastItemId; + } + return false; +} + +bool ImGui::IsItemFocused() +{ + ImGuiContext& g = *GImGui; + return g.NavId && !g.NavDisableHighlight && g.NavId == g.CurrentWindow->DC.LastItemId; +} + +bool ImGui::IsItemClicked(int mouse_button) +{ + return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_Default); +} + +bool ImGui::IsAnyItemHovered() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; +} + +bool ImGui::IsAnyItemActive() +{ + ImGuiContext& g = *GImGui; + return g.ActiveId != 0; +} + +bool ImGui::IsAnyItemFocused() +{ + ImGuiContext& g = *GImGui; + return g.NavId != 0 && !g.NavDisableHighlight; +} + +bool ImGui::IsItemVisible() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ClipRect.Overlaps(window->DC.LastItemRect); +} + +// Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. +void ImGui::SetItemAllowOverlap() +{ + ImGuiContext& g = *GImGui; + if (g.HoveredId == g.CurrentWindow->DC.LastItemId) + g.HoveredIdAllowOverlap = true; + if (g.ActiveId == g.CurrentWindow->DC.LastItemId) + g.ActiveIdAllowOverlap = true; +} + +ImVec2 ImGui::GetItemRectMin() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.LastItemRect.Min; +} + +ImVec2 ImGui::GetItemRectMax() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.LastItemRect.Max; +} + +ImVec2 ImGui::GetItemRectSize() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.LastItemRect.GetSize(); +} + +static ImRect GetViewportRect() +{ + ImGuiContext& g = *GImGui; + if (g.IO.DisplayVisibleMin.x != g.IO.DisplayVisibleMax.x && g.IO.DisplayVisibleMin.y != g.IO.DisplayVisibleMax.y) + return ImRect(g.IO.DisplayVisibleMin, g.IO.DisplayVisibleMax); + return ImRect(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); +} + +// Not exposed publicly as BeginTooltip() because bool parameters are evil. Let's see if other needs arise first. +void ImGui::BeginTooltipEx(ImGuiWindowFlags extra_flags, bool override_previous_tooltip) +{ + ImGuiContext& g = *GImGui; + char window_name[16]; + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); + if (override_previous_tooltip) + if (ImGuiWindow* window = FindWindowByName(window_name)) + if (window->Active) + { + // Hide previous tooltips. We can't easily "reset" the content of a window so we create a new one. + window->HiddenFrames = 1; + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); + } + ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip|ImGuiWindowFlags_NoInputs|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoNav; + Begin(window_name, NULL, flags | extra_flags); +} + +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + BeginTooltipEx(0, true); + TextV(fmt, args); + EndTooltip(); +} + +void ImGui::SetTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::BeginTooltip() +{ + BeginTooltipEx(0, false); +} + +void ImGui::EndTooltip() +{ + IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls + End(); +} + +// Mark popup as open (toggle toward open state). +// Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. +// Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). +// One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) +void ImGui::OpenPopupEx(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + int current_stack_size = g.CurrentPopupStack.Size; + ImGuiPopupRef popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. + popup_ref.PopupId = id; + popup_ref.Window = NULL; + popup_ref.ParentWindow = parent_window; + popup_ref.OpenFrameCount = g.FrameCount; + popup_ref.OpenParentId = parent_window->IDStack.back(); + popup_ref.OpenMousePos = g.IO.MousePos; + popup_ref.OpenPopupPos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; + + if (g.OpenPopupStack.Size < current_stack_size + 1) + { + g.OpenPopupStack.push_back(popup_ref); + } + else + { + // Close child popups if any + g.OpenPopupStack.resize(current_stack_size + 1); + + // Gently handle the user mistakenly calling OpenPopup() every frame. It is a programming mistake! However, if we were to run the regular code path, the ui + // would become completely unusable because the popup will always be in hidden-while-calculating-size state _while_ claiming focus. Which would be a very confusing + // situation for the programmer. Instead, we silently allow the popup to proceed, it will keep reappearing and the programming error will be more obvious to understand. + if (g.OpenPopupStack[current_stack_size].PopupId == id && g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) + g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; + else + g.OpenPopupStack[current_stack_size] = popup_ref; + + // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). + // This is equivalent to what ClosePopupToLevel() does. + //if (g.OpenPopupStack[current_stack_size].PopupId == id) + // FocusWindow(parent_window); + } +} + +void ImGui::OpenPopup(const char* str_id) +{ + ImGuiContext& g = *GImGui; + OpenPopupEx(g.CurrentWindow->GetID(str_id)); +} + +void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.empty()) + return; + + // When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. + // Don't close our own child popup windows. + int n = 0; + if (ref_window) + { + for (n = 0; n < g.OpenPopupStack.Size; n++) + { + ImGuiPopupRef& popup = g.OpenPopupStack[n]; + if (!popup.Window) + continue; + IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); + if (popup.Window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + + // Trim the stack if popups are not direct descendant of the reference window (which is often the NavWindow) + bool has_focus = false; + for (int m = n; m < g.OpenPopupStack.Size && !has_focus; m++) + has_focus = (g.OpenPopupStack[m].Window && g.OpenPopupStack[m].Window->RootWindow == ref_window->RootWindow); + if (!has_focus) + break; + } + } + if (n < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the block below + ClosePopupToLevel(n); +} + +static ImGuiWindow* GetFrontMostModalRootWindow() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size-1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if (popup->Flags & ImGuiWindowFlags_Modal) + return popup; + return NULL; +} + +static void ClosePopupToLevel(int remaining) +{ + IM_ASSERT(remaining >= 0); + ImGuiContext& g = *GImGui; + ImGuiWindow* focus_window = (remaining > 0) ? g.OpenPopupStack[remaining-1].Window : g.OpenPopupStack[0].ParentWindow; + if (g.NavLayer == 0) + focus_window = NavRestoreLastChildNavWindow(focus_window); + ImGui::FocusWindow(focus_window); + focus_window->DC.NavHideHighlightOneFrame = true; + g.OpenPopupStack.resize(remaining); +} + +void ImGui::ClosePopup(ImGuiID id) +{ + if (!IsPopupOpen(id)) + return; + ImGuiContext& g = *GImGui; + ClosePopupToLevel(g.OpenPopupStack.Size - 1); +} + +// Close the popup we have begin-ed into. +void ImGui::CloseCurrentPopup() +{ + ImGuiContext& g = *GImGui; + int popup_idx = g.CurrentPopupStack.Size - 1; + if (popup_idx < 0 || popup_idx >= g.OpenPopupStack.Size || g.CurrentPopupStack[popup_idx].PopupId != g.OpenPopupStack[popup_idx].PopupId) + return; + while (popup_idx > 0 && g.OpenPopupStack[popup_idx].Window && (g.OpenPopupStack[popup_idx].Window->Flags & ImGuiWindowFlags_ChildMenu)) + popup_idx--; + ClosePopupToLevel(popup_idx); +} + +bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(id)) + { + g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values + return false; + } + + char name[20]; + if (extra_flags & ImGuiWindowFlags_ChildMenu) + ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth + else + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame + + bool is_open = Begin(name, NULL, extra_flags | ImGuiWindowFlags_Popup); + if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) + EndPopup(); + + return is_open; +} + +bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= g.CurrentPopupStack.Size) // Early out for performance + { + g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values + return false; + } + return BeginPopupEx(g.CurrentWindow->GetID(str_id), flags|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::IsPopupOpen(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == id; +} + +bool ImGui::IsPopupOpen(const char* str_id) +{ + ImGuiContext& g = *GImGui; + return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == g.CurrentWindow->GetID(str_id); +} + +bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = window->GetID(name); + if (!IsPopupOpen(id)) + { + g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values + return false; + } + + // Center modal windows by default + // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. + if (g.NextWindowData.PosCond == 0) + SetNextWindowPos(g.IO.DisplaySize * 0.5f, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + bool is_open = Begin(name, p_open, flags | ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); + if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + { + EndPopup(); + if (is_open) + ClosePopup(id); + return false; + } + + return is_open; +} + +static void NavProcessMoveRequestWrapAround(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.NavWindow == window && NavMoveRequestButNoResultYet()) + if ((g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) && g.NavMoveRequestForward == ImGuiNavForward_None && g.NavLayer == 0) + { + g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; + ImGui::NavMoveRequestCancel(); + g.NavWindow->NavRectRel[0].Min.y = g.NavWindow->NavRectRel[0].Max.y = ((g.NavMoveDir == ImGuiDir_Up) ? ImMax(window->SizeFull.y, window->SizeContents.y) : 0.0f) - window->Scroll.y; + } +} + +void ImGui::EndPopup() +{ + ImGuiContext& g = *GImGui; (void)g; + IM_ASSERT(g.CurrentWindow->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls + IM_ASSERT(g.CurrentPopupStack.Size > 0); + + // Make all menus and popups wrap around for now, may need to expose that policy. + NavProcessMoveRequestWrapAround(g.CurrentWindow); + + End(); +} + +bool ImGui::OpenPopupOnItemClick(const char* str_id, int mouse_button) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + { + ImGuiID id = str_id ? window->GetID(str_id) : window->DC.LastItemId; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // However, you cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + OpenPopupEx(id); + return true; + } + return false; +} + +// This is a helper to handle the simplest case of associating one named popup to one given widget. +// You may want to handle this on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). +// You can pass a NULL str_id to use the identifier of the last item. +bool ImGui::BeginPopupContextItem(const char* str_id, int mouse_button) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + ImGuiID id = str_id ? window->GetID(str_id) : window->DC.LastItemId; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // However, you cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + OpenPopupEx(id); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextWindow(const char* str_id, int mouse_button, bool also_over_items) +{ + if (!str_id) + str_id = "window_context"; + ImGuiID id = GImGui->CurrentWindow->GetID(str_id); + if (IsMouseReleased(mouse_button) && IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + if (also_over_items || !IsAnyItemHovered()) + OpenPopupEx(id); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextVoid(const char* str_id, int mouse_button) +{ + if (!str_id) + str_id = "void_context"; + ImGuiID id = GImGui->CurrentWindow->GetID(str_id); + if (IsMouseReleased(mouse_button) && !IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) + OpenPopupEx(id); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); +} + +static bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = ImGui::GetCurrentWindow(); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_ChildWindow; + flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + + const ImVec2 content_avail = ImGui::GetContentRegionAvail(); + ImVec2 size = ImFloor(size_arg); + const int auto_fit_axises = ((size.x == 0.0f) ? (1 << ImGuiAxis_X) : 0x00) | ((size.y == 0.0f) ? (1 << ImGuiAxis_Y) : 0x00); + if (size.x <= 0.0f) + size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too much issues) + if (size.y <= 0.0f) + size.y = ImMax(content_avail.y + size.y, 4.0f); + + const float backup_border_size = g.Style.ChildBorderSize; + if (!border) + g.Style.ChildBorderSize = 0.0f; + flags |= extra_flags; + + char title[256]; + if (name) + ImFormatString(title, IM_ARRAYSIZE(title), "%s/%s_%08X", parent_window->Name, name, id); + else + ImFormatString(title, IM_ARRAYSIZE(title), "%s/%08X", parent_window->Name, id); + + ImGui::SetNextWindowSize(size); + bool ret = ImGui::Begin(title, NULL, flags); + ImGuiWindow* child_window = ImGui::GetCurrentWindow(); + child_window->ChildId = id; + child_window->AutoFitChildAxises = auto_fit_axises; + g.Style.ChildBorderSize = backup_border_size; + + // Process navigation-in immediately so NavInit can run on first frame + if (!(flags & ImGuiWindowFlags_NavFlattened) && (child_window->DC.NavLayerActiveMask != 0 || child_window->DC.NavHasScroll) && g.NavActivateId == id) + { + ImGui::FocusWindow(child_window); + ImGui::NavInitWindow(child_window, false); + ImGui::SetActiveID(id+1, child_window); // Steal ActiveId with a dummy id so that key-press won't activate child item + g.ActiveIdSource = ImGuiInputSource_Nav; + } + + return ret; +} + +bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); +} + +bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) +{ + IM_ASSERT(id != 0); + return BeginChildEx(NULL, id, size_arg, border, extra_flags); +} + +void ImGui::EndChild() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() callss + if (window->BeginCount > 1) + { + End(); + } + else + { + // When using auto-filling child window, we don't provide full width/height to ItemSize so that it doesn't feed back into automatic size-fitting. + ImVec2 sz = GetWindowSize(); + if (window->AutoFitChildAxises & (1 << ImGuiAxis_X)) // Arbitrary minimum zero-ish child size of 4.0f causes less trouble than a 0.0f + sz.x = ImMax(4.0f, sz.x); + if (window->AutoFitChildAxises & (1 << ImGuiAxis_Y)) + sz.y = ImMax(4.0f, sz.y); + End(); + + ImGuiWindow* parent_window = g.CurrentWindow; + ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + sz); + ItemSize(sz); + if ((window->DC.NavLayerActiveMask != 0 || window->DC.NavHasScroll) && !(window->Flags & ImGuiWindowFlags_NavFlattened)) + { + ItemAdd(bb, window->ChildId); + RenderNavHighlight(bb, window->ChildId); + + // When browsing a window that has no activable items (scroll only) we keep a highlight on the child + if (window->DC.NavLayerActiveMask == 0 && window == g.NavWindow) + RenderNavHighlight(ImRect(bb.Min - ImVec2(2,2), bb.Max + ImVec2(2,2)), g.NavId, ImGuiNavHighlightFlags_TypeThin); + } + else + { + // Not navigable into + ItemAdd(bb, 0); + } + } +} + +// Helper to create a child window / scrolling region that looks like a normal widget frame. +bool ImGui::BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); + return BeginChild(id, size, true, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysUseWindowPadding | extra_flags); +} + +void ImGui::EndChildFrame() +{ + EndChild(); + PopStyleVar(3); + PopStyleColor(); +} + +// Save and compare stack sizes on Begin()/End() to detect usage errors +static void CheckStacksSize(ImGuiWindow* window, bool write) +{ + // NOT checking: DC.ItemWidth, DC.AllowKeyboardFocus, DC.ButtonRepeat, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) + ImGuiContext& g = *GImGui; + int* p_backup = &window->DC.StackSizesBackup[0]; + { int current = window->IDStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushID/PopID or TreeNode/TreePop Mismatch!"); p_backup++; } // Too few or too many PopID()/TreePop() + { int current = window->DC.GroupStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "BeginGroup/EndGroup Mismatch!"); p_backup++; } // Too few or too many EndGroup() + { int current = g.CurrentPopupStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "BeginMenu/EndMenu or BeginPopup/EndPopup Mismatch"); p_backup++;}// Too few or too many EndMenu()/EndPopup() + { int current = g.ColorModifiers.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushStyleColor/PopStyleColor Mismatch!"); p_backup++; } // Too few or too many PopStyleColor() + { int current = g.StyleModifiers.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushStyleVar/PopStyleVar Mismatch!"); p_backup++; } // Too few or too many PopStyleVar() + { int current = g.FontStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushFont/PopFont Mismatch!"); p_backup++; } // Too few or too many PopFont() + IM_ASSERT(p_backup == window->DC.StackSizesBackup + IM_ARRAYSIZE(window->DC.StackSizesBackup)); +} + +enum ImGuiPopupPositionPolicy +{ + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox +}; + +static ImVec2 FindBestWindowPosForPopup(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy = ImGuiPopupPositionPolicy_Default) +{ + const ImGuiStyle& style = GImGui->Style; + + // r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) + // r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. + ImVec2 safe_padding = style.DisplaySafeAreaPadding; + ImRect r_outer(GetViewportRect()); + r_outer.Expand(ImVec2((size.x - r_outer.GetWidth() > safe_padding.x*2) ? -safe_padding.x : 0.0f, (size.y - r_outer.GetHeight() > safe_padding.y*2) ? -safe_padding.y : 0.0f)); + ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); + //GImGui->OverlayDrawList.AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); + //GImGui->OverlayDrawList.AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + + // Combo Box policy (we want a connecting edge) + if (policy == ImGuiPopupPositionPolicy_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Default popup policy + const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); + float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); + if (avail_w < size.x || avail_h < size.y) + continue; + ImVec2 pos; + pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; + pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; + *last_dir = dir; + return pos; + } + + // Fallback, try to keep within display + *last_dir = ImGuiDir_None; + ImVec2 pos = ref_pos; + pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); + pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); + return pos; +} + +static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) +{ + window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); + window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); + window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); +} + +ImGuiWindow* ImGui::FindWindowByName(const char* name) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = ImHash(name, 0); + return (ImGuiWindow*)g.WindowsById.GetVoidPtr(id); +} + +static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + + // Create window the first time + ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); + window->Flags = flags; + g.WindowsById.SetVoidPtr(window->ID, window); + + // User can disable loading and saving of settings. Tooltip and child windows also don't store settings. + if (!(flags & ImGuiWindowFlags_NoSavedSettings)) + { + // Retrieve settings from .ini file + // Use SetWindowPos() or SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + window->Pos = window->PosFloat = ImVec2(60, 60); + + if (ImGuiWindowSettings* settings = ImGui::FindWindowSettings(window->ID)) + { + SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); + window->PosFloat = settings->Pos; + window->Pos = ImFloor(window->PosFloat); + window->Collapsed = settings->Collapsed; + if (ImLengthSqr(settings->Size) > 0.00001f) + size = settings->Size; + } + } + window->Size = window->SizeFull = window->SizeFullAtLastBegin = size; + + if ((flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) + { + window->AutoFitFramesX = window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } + else + { + if (window->Size.x <= 0.0f) + window->AutoFitFramesX = 2; + if (window->Size.y <= 0.0f) + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); + } + + if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) + g.Windows.insert(g.Windows.begin(), window); // Quite slow but rare and only once + else + g.Windows.push_back(window); + return window; +} + +static ImVec2 CalcSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size) +{ + ImGuiContext& g = *GImGui; + if (g.NextWindowData.SizeConstraintCond != 0) + { + // Using -1,-1 on either X/Y axis to preserve the current size. + ImRect cr = g.NextWindowData.SizeConstraintRect; + new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; + new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; + if (g.NextWindowData.SizeCallback) + { + ImGuiSizeCallbackData data; + data.UserData = g.NextWindowData.SizeCallbackUserData; + data.Pos = window->Pos; + data.CurrentSize = window->SizeFull; + data.DesiredSize = new_size; + g.NextWindowData.SizeCallback(&data); + new_size = data.DesiredSize; + } + } + + // Minimum size + if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) + { + new_size = ImMax(new_size, g.Style.WindowMinSize); + new_size.y = ImMax(new_size.y, window->TitleBarHeight() + window->MenuBarHeight() + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); // Reduce artifacts with very small windows + } + return new_size; +} + +static ImVec2 CalcSizeContents(ImGuiWindow* window) +{ + ImVec2 sz; + sz.x = (float)(int)((window->SizeContentsExplicit.x != 0.0f) ? window->SizeContentsExplicit.x : (window->DC.CursorMaxPos.x - window->Pos.x + window->Scroll.x)); + sz.y = (float)(int)((window->SizeContentsExplicit.y != 0.0f) ? window->SizeContentsExplicit.y : (window->DC.CursorMaxPos.y - window->Pos.y + window->Scroll.y)); + return sz + window->WindowPadding; +} + +static ImVec2 CalcSizeAutoFit(ImGuiWindow* window, const ImVec2& size_contents) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + ImVec2 size_auto_fit; + if ((flags & ImGuiWindowFlags_Tooltip) != 0) + { + // Tooltip always resize. We keep the spacing symmetric on both axises for aesthetic purpose. + size_auto_fit = size_contents; + } + else + { + // When the window cannot fit all contents (either because of constraints, either because screen is too small): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. + size_auto_fit = ImClamp(size_contents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); + ImVec2 size_auto_fit_after_constraint = CalcSizeAfterConstraint(window, size_auto_fit); + if (size_auto_fit_after_constraint.x < size_contents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) + size_auto_fit.y += style.ScrollbarSize; + if (size_auto_fit_after_constraint.y < size_contents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) + size_auto_fit.x += style.ScrollbarSize; + } + return size_auto_fit; +} + +static float GetScrollMaxX(ImGuiWindow* window) +{ + return ImMax(0.0f, window->SizeContents.x - (window->SizeFull.x - window->ScrollbarSizes.x)); +} + +static float GetScrollMaxY(ImGuiWindow* window) +{ + return ImMax(0.0f, window->SizeContents.y - (window->SizeFull.y - window->ScrollbarSizes.y)); +} + +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) +{ + ImVec2 scroll = window->Scroll; + float cr_x = window->ScrollTargetCenterRatio.x; + float cr_y = window->ScrollTargetCenterRatio.y; + if (window->ScrollTarget.x < FLT_MAX) + scroll.x = window->ScrollTarget.x - cr_x * (window->SizeFull.x - window->ScrollbarSizes.x); + if (window->ScrollTarget.y < FLT_MAX) + scroll.y = window->ScrollTarget.y - (1.0f - cr_y) * (window->TitleBarHeight() + window->MenuBarHeight()) - cr_y * (window->SizeFull.y - window->ScrollbarSizes.y); + scroll = ImMax(scroll, ImVec2(0.0f, 0.0f)); + if (!window->Collapsed && !window->SkipItems) + { + scroll.x = ImMin(scroll.x, GetScrollMaxX(window)); + scroll.y = ImMin(scroll.y, GetScrollMaxY(window)); + } + return scroll; +} + +static ImGuiCol GetWindowBgColorIdxFromFlags(ImGuiWindowFlags flags) +{ + if (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + return ImGuiCol_PopupBg; + if (flags & ImGuiWindowFlags_ChildWindow) + return ImGuiCol_ChildBg; + return ImGuiCol_WindowBg; +} + +static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size) +{ + ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left + ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right + ImVec2 size_expected = pos_max - pos_min; + ImVec2 size_constrained = CalcSizeAfterConstraint(window, size_expected); + *out_pos = pos_min; + if (corner_norm.x == 0.0f) + out_pos->x -= (size_constrained.x - size_expected.x); + if (corner_norm.y == 0.0f) + out_pos->y -= (size_constrained.y - size_expected.y); + *out_size = size_constrained; +} + +struct ImGuiResizeGripDef +{ + ImVec2 CornerPos; + ImVec2 InnerDir; + int AngleMin12, AngleMax12; +}; + +const ImGuiResizeGripDef resize_grip_def[4] = +{ + { ImVec2(1,1), ImVec2(-1,-1), 0, 3 }, // Lower right + { ImVec2(0,1), ImVec2(+1,-1), 3, 6 }, // Lower left + { ImVec2(0,0), ImVec2(+1,+1), 6, 9 }, // Upper left + { ImVec2(1,0), ImVec2(-1,+1), 9,12 }, // Upper right +}; + +static ImRect GetBorderRect(ImGuiWindow* window, int border_n, float perp_padding, float thickness) +{ + ImRect rect = window->Rect(); + if (thickness == 0.0f) rect.Max -= ImVec2(1,1); + if (border_n == 0) return ImRect(rect.Min.x + perp_padding, rect.Min.y, rect.Max.x - perp_padding, rect.Min.y + thickness); + if (border_n == 1) return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x, rect.Max.y - perp_padding); + if (border_n == 2) return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y); + if (border_n == 3) return ImRect(rect.Min.x, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); + IM_ASSERT(0); + return ImRect(); +} + +// Handle resize for: Resize Grips, Borders, Gamepad +static void ImGui::UpdateManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4]) +{ + ImGuiContext& g = *GImGui; + ImGuiWindowFlags flags = window->Flags; + if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + return; + + const int resize_border_count = (flags & ImGuiWindowFlags_ResizeFromAnySide) ? 4 : 0; + const float grip_draw_size = (float)(int)ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); + const float grip_hover_size = (float)(int)(grip_draw_size * 0.75f); + + ImVec2 pos_target(FLT_MAX, FLT_MAX); + ImVec2 size_target(FLT_MAX, FLT_MAX); + + // Manual resize grips + PushID("#RESIZE"); + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPos); + + // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window + ImRect resize_rect(corner, corner + grip.InnerDir * grip_hover_size); + resize_rect.FixInverted(); + bool hovered, held; + ButtonBehavior(resize_rect, window->GetID((void*)(intptr_t)resize_grip_n), &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + if (hovered || held) + g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; + + if (g.HoveredWindow == window && held && g.IO.MouseDoubleClicked[0] && resize_grip_n == 0) + { + // Manual auto-fit when double-clicking + size_target = CalcSizeAfterConstraint(window, size_auto_fit); + ClearActiveID(); + } + else if (held) + { + // Resize from any of the four corners + // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position + ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + resize_rect.GetSize() * grip.CornerPos; // Corner of the window corresponding to our corner grip + CalcResizePosSizeFromAnyCorner(window, corner_target, grip.CornerPos, &pos_target, &size_target); + } + if (resize_grip_n == 0 || held || hovered) + resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); + } + for (int border_n = 0; border_n < resize_border_count; border_n++) + { + const float BORDER_SIZE = 5.0f; // FIXME: Only works _inside_ window because of HoveredWindow check. + const float BORDER_APPEAR_TIMER = 0.05f; // Reduce visual noise + bool hovered, held; + ImRect border_rect = GetBorderRect(window, border_n, grip_hover_size, BORDER_SIZE); + ButtonBehavior(border_rect, window->GetID((void*)(intptr_t)(border_n + 4)), &hovered, &held, ImGuiButtonFlags_FlattenChildren); + if ((hovered && g.HoveredIdTimer > BORDER_APPEAR_TIMER) || held) + { + g.MouseCursor = (border_n & 1) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; + if (held) *border_held = border_n; + } + if (held) + { + ImVec2 border_target = window->Pos; + ImVec2 border_posn; + if (border_n == 0) { border_posn = ImVec2(0, 0); border_target.y = (g.IO.MousePos.y - g.ActiveIdClickOffset.y); } + if (border_n == 1) { border_posn = ImVec2(1, 0); border_target.x = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + BORDER_SIZE); } + if (border_n == 2) { border_posn = ImVec2(0, 1); border_target.y = (g.IO.MousePos.y - g.ActiveIdClickOffset.y + BORDER_SIZE); } + if (border_n == 3) { border_posn = ImVec2(0, 0); border_target.x = (g.IO.MousePos.x - g.ActiveIdClickOffset.x); } + CalcResizePosSizeFromAnyCorner(window, border_target, border_posn, &pos_target, &size_target); + } + } + PopID(); + + // Navigation/gamepad resize + if (g.NavWindowingTarget == window) + { + ImVec2 nav_resize_delta; + if (g.NavWindowingInputSource == ImGuiInputSource_NavKeyboard && g.IO.KeyShift) + nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard, ImGuiInputReadMode_Down); + if (g.NavWindowingInputSource == ImGuiInputSource_NavGamepad) + nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_Down); + if (nav_resize_delta.x != 0.0f || nav_resize_delta.y != 0.0f) + { + const float NAV_RESIZE_SPEED = 600.0f; + nav_resize_delta *= ImFloor(NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); + g.NavWindowingToggleLayer = false; + g.NavDisableMouseHover = true; + resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); + // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. + size_target = CalcSizeAfterConstraint(window, window->SizeFull + nav_resize_delta); + } + } + + // Apply back modified position/size to window + if (size_target.x != FLT_MAX) + { + window->SizeFull = size_target; + MarkIniSettingsDirty(window); + } + if (pos_target.x != FLT_MAX) + { + window->Pos = window->PosFloat = ImFloor(pos_target); + MarkIniSettingsDirty(window); + } + + window->Size = window->SizeFull; +} + +// Push a new ImGui window to add widgets to. +// - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. +// - Begin/End can be called multiple times during the frame with the same window name to append content. +// - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file). +// You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file. +// - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui::End() even if false is returned. +// - Passing 'bool* p_open' displays a Close button on the upper-right corner of the window, the pointed value will be set to false when the button is pressed. +bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + IM_ASSERT(name != NULL); // Window name required + IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() + IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet + + // Find or create + ImGuiWindow* window = FindWindowByName(name); + if (!window) + { + ImVec2 size_on_first_use = (g.NextWindowData.SizeCond != 0) ? g.NextWindowData.SizeVal : ImVec2(0.0f, 0.0f); // Any condition flag will do since we are creating a new window here. + window = CreateNewWindow(name, size_on_first_use, flags); + } + + // Automatically disable manual moving/resizing when NoInputs is set + if (flags & ImGuiWindowFlags_NoInputs) + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + + if (flags & ImGuiWindowFlags_NavFlattened) + IM_ASSERT(flags & ImGuiWindowFlags_ChildWindow); + + const int current_frame = g.FrameCount; + const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); + if (first_begin_of_the_frame) + window->Flags = (ImGuiWindowFlags)flags; + else + flags = window->Flags; + + // Update the Appearing flag + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on + const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFrames == 1); + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupRef& popup_ref = g.OpenPopupStack[g.CurrentPopupStack.Size]; + window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed + window_just_activated_by_user |= (window != popup_ref.Window); + } + window->Appearing = (window_just_activated_by_user || window_just_appearing_after_hidden_for_resize); + window->CloseButton = (p_open != NULL); + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack + ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back(); + ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; + IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); + + // Add to stack + g.CurrentWindowStack.push_back(window); + SetCurrentWindow(window); + CheckStacksSize(window, true); + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupRef& popup_ref = g.OpenPopupStack[g.CurrentPopupStack.Size]; + popup_ref.Window = window; + g.CurrentPopupStack.push_back(popup_ref); + window->PopupId = popup_ref.PopupId; + } + + if (window_just_appearing_after_hidden_for_resize && !(flags & ImGuiWindowFlags_ChildWindow)) + window->NavLastIds[0] = 0; + + // Process SetNextWindow***() calls + bool window_pos_set_by_api = false; + bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; + if (g.NextWindowData.PosCond) + { + window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; + if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) + { + // May be processed on the next frame if this is our first frame and we are measuring size + // FIXME: Look into removing the branch so everything can go through this same code path for consistency. + window->SetWindowPosVal = g.NextWindowData.PosVal; + window->SetWindowPosPivot = g.NextWindowData.PosPivotVal; + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + } + else + { + SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); + } + g.NextWindowData.PosCond = 0; + } + if (g.NextWindowData.SizeCond) + { + window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); + window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); + g.NextWindowData.SizeCond = 0; + } + if (g.NextWindowData.ContentSizeCond) + { + // Adjust passed "client size" to become a "window size" + window->SizeContentsExplicit = g.NextWindowData.ContentSizeVal; + if (window->SizeContentsExplicit.y != 0.0f) + window->SizeContentsExplicit.y += window->TitleBarHeight() + window->MenuBarHeight(); + g.NextWindowData.ContentSizeCond = 0; + } + else if (first_begin_of_the_frame) + { + window->SizeContentsExplicit = ImVec2(0.0f, 0.0f); + } + if (g.NextWindowData.CollapsedCond) + { + SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); + g.NextWindowData.CollapsedCond = 0; + } + if (g.NextWindowData.FocusCond) + { + SetWindowFocus(); + g.NextWindowData.FocusCond = 0; + } + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + + // When reusing window again multiple times a frame, just append content (don't need to setup again) + if (first_begin_of_the_frame) + { + const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) + + // Initialize + window->ParentWindow = parent_window; + window->RootWindow = window->RootWindowForTitleBarHighlight = window->RootWindowForTabbing = window->RootWindowForNav = window; + if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !window_is_child_tooltip) + window->RootWindow = parent_window->RootWindow; + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + window->RootWindowForTitleBarHighlight = window->RootWindowForTabbing = parent_window->RootWindowForTitleBarHighlight; // Same value in master branch, will differ for docking + while (window->RootWindowForNav->Flags & ImGuiWindowFlags_NavFlattened) + window->RootWindowForNav = window->RootWindowForNav->ParentWindow; + + window->Active = true; + window->BeginOrderWithinParent = 0; + window->BeginOrderWithinContext = g.WindowsActiveCount++; + window->BeginCount = 0; + window->ClipRect = ImVec4(-FLT_MAX,-FLT_MAX,+FLT_MAX,+FLT_MAX); + window->LastFrameActive = current_frame; + window->IDStack.resize(1); + + // Lock window rounding, border size and rounding so that altering the border sizes for children doesn't have side-effects. + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + window->WindowBorderSize = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildBorderSize : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; + window->WindowPadding = style.WindowPadding; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & (ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_Popup)) && window->WindowBorderSize == 0.0f) + window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); + + // Collapse window by double-clicking on title bar + // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + { + ImRect title_bar_rect = window->TitleBarRect(); + if (window->CollapseToggleWanted || (g.HoveredWindow == window && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) && g.IO.MouseDoubleClicked[0])) + { + window->Collapsed = !window->Collapsed; + MarkIniSettingsDirty(window); + FocusWindow(window); + } + } + else + { + window->Collapsed = false; + } + window->CollapseToggleWanted = false; + + // SIZE + + // Update contents size from last frame for auto-fitting (unless explicitly specified) + window->SizeContents = CalcSizeContents(window); + + // Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows) + if (window->HiddenFrames > 0) + window->HiddenFrames--; + if ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0 && window_just_activated_by_user) + { + window->HiddenFrames = 1; + if (flags & ImGuiWindowFlags_AlwaysAutoResize) + { + if (!window_size_x_set_by_api) + window->Size.x = window->SizeFull.x = 0.f; + if (!window_size_y_set_by_api) + window->Size.y = window->SizeFull.y = 0.f; + window->SizeContents = ImVec2(0.f, 0.f); + } + } + + // Calculate auto-fit size, handle automatic resize + const ImVec2 size_auto_fit = CalcSizeAutoFit(window, window->SizeContents); + ImVec2 size_full_modified(FLT_MAX, FLT_MAX); + if (flags & ImGuiWindowFlags_AlwaysAutoResize && !window->Collapsed) + { + // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. + if (!window_size_x_set_by_api) + window->SizeFull.x = size_full_modified.x = size_auto_fit.x; + if (!window_size_y_set_by_api) + window->SizeFull.y = size_full_modified.y = size_auto_fit.y; + } + else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + { + // Auto-fit only grows during the first few frames + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. + if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) + window->SizeFull.x = size_full_modified.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) + window->SizeFull.y = size_full_modified.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + if (!window->Collapsed) + MarkIniSettingsDirty(window); + } + + // Apply minimum/maximum window size constraints and final size + window->SizeFull = CalcSizeAfterConstraint(window, window->SizeFull); + window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; + + // SCROLLBAR STATUS + + // Update scrollbar status (based on the Size that was effective during last frame or the auto-resized Size). + if (!window->Collapsed) + { + // When reading the current size we need to read it after size constraints have been applied + float size_x_for_scrollbars = size_full_modified.x != FLT_MAX ? window->SizeFull.x : window->SizeFullAtLastBegin.x; + float size_y_for_scrollbars = size_full_modified.y != FLT_MAX ? window->SizeFull.y : window->SizeFullAtLastBegin.y; + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + if (window->ScrollbarX && !window->ScrollbarY) + window->ScrollbarY = (window->SizeContents.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); + } + + // POSITION + + // Popup latch its initial position, will position itself when it appears next frame + if (window_just_activated_by_user) + { + window->AutoPosLastDirection = ImGuiDir_None; + if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api) + window->Pos = window->PosFloat = g.CurrentPopupStack.back().OpenPopupPos; + } + + // Position child window + if (flags & ImGuiWindowFlags_ChildWindow) + { + window->BeginOrderWithinParent = parent_window->DC.ChildWindows.Size; + parent_window->DC.ChildWindows.push_back(window); + if (!(flags & ImGuiWindowFlags_Popup) && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = window->PosFloat = parent_window->DC.CursorPos; + } + + const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFrames == 0); + if (window_pos_with_pivot) + { + // Position given a pivot (e.g. for centering) + SetWindowPos(window, ImMax(style.DisplaySafeAreaPadding, window->SetWindowPosVal - window->SizeFull * window->SetWindowPosPivot), 0); + } + else if (flags & ImGuiWindowFlags_ChildMenu) + { + // Child menus typically request _any_ position within the parent menu item, and then our FindBestPopupWindowPos() function will move the new menu outside the parent bounds. + // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. + IM_ASSERT(window_pos_set_by_api); + float horizontal_overlap = style.ItemSpacing.x; // We want some overlap to convey the relative depth of each popup (currently the amount of overlap it is hard-coded to style.ItemSpacing.x, may need to introduce another style value). + ImGuiWindow* parent_menu = parent_window_in_stack; + ImRect rect_to_avoid; + if (parent_menu->DC.MenuBarAppending) + rect_to_avoid = ImRect(-FLT_MAX, parent_menu->Pos.y + parent_menu->TitleBarHeight(), FLT_MAX, parent_menu->Pos.y + parent_menu->TitleBarHeight() + parent_menu->MenuBarHeight()); + else + rect_to_avoid = ImRect(parent_menu->Pos.x + horizontal_overlap, -FLT_MAX, parent_menu->Pos.x + parent_menu->Size.x - horizontal_overlap - parent_menu->ScrollbarSizes.x, FLT_MAX); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + } + else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) + { + ImRect rect_to_avoid(window->PosFloat.x - 1, window->PosFloat.y - 1, window->PosFloat.x + 1, window->PosFloat.y + 1); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + } + + // Position tooltip (always follows mouse) + if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) + { + float sc = g.Style.MouseCursorScale; + ImVec2 ref_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; + ImRect rect_to_avoid; + if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.NavFlags & ImGuiNavFlags_MoveMouse)) + rect_to_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); + else + rect_to_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * sc, ref_pos.y + 24 * sc); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + window->PosFloat = FindBestWindowPosForPopup(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + if (window->AutoPosLastDirection == ImGuiDir_None) + window->PosFloat = ref_pos + ImVec2(2,2); // If there's not enough room, for tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. + } + + // Clamp position so it stays visible + if (!(flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) + { + if (!window_pos_set_by_api && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && g.IO.DisplaySize.x > 0.0f && g.IO.DisplaySize.y > 0.0f) // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + { + ImVec2 padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + window->PosFloat = ImMax(window->PosFloat + window->Size, padding) - window->Size; + window->PosFloat = ImMin(window->PosFloat, g.IO.DisplaySize - padding); + } + } + window->Pos = ImFloor(window->PosFloat); + + // Default item width. Make it proportional to window size if window manually resizes + if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) + window->ItemWidthDefault = (float)(int)(window->Size.x * 0.65f); + else + window->ItemWidthDefault = (float)(int)(g.FontSize * 16.0f); + + // Prepare for focus requests + window->FocusIdxAllRequestCurrent = (window->FocusIdxAllRequestNext == INT_MAX || window->FocusIdxAllCounter == -1) ? INT_MAX : (window->FocusIdxAllRequestNext + (window->FocusIdxAllCounter+1)) % (window->FocusIdxAllCounter+1); + window->FocusIdxTabRequestCurrent = (window->FocusIdxTabRequestNext == INT_MAX || window->FocusIdxTabCounter == -1) ? INT_MAX : (window->FocusIdxTabRequestNext + (window->FocusIdxTabCounter+1)) % (window->FocusIdxTabCounter+1); + window->FocusIdxAllCounter = window->FocusIdxTabCounter = -1; + window->FocusIdxAllRequestNext = window->FocusIdxTabRequestNext = INT_MAX; + + // Apply scrolling + window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); + window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + + // Apply focus, new windows appears in front + bool want_focus = false; + if (window_just_activated_by_user && !(flags & ImGuiWindowFlags_NoFocusOnAppearing)) + if (!(flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) || (flags & ImGuiWindowFlags_Popup)) + want_focus = true; + + // Handle manual resize: Resize Grips, Borders, Gamepad + int border_held = -1; + ImU32 resize_grip_col[4] = { 0 }; + const int resize_grip_count = (flags & ImGuiWindowFlags_ResizeFromAnySide) ? 2 : 1; // 4 + const float grip_draw_size = (float)(int)ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); + if (!window->Collapsed) + UpdateManualResize(window, size_auto_fit, &border_held, resize_grip_count, &resize_grip_col[0]); + + // DRAWING + + // Setup draw list and outer clipping rectangle + window->DrawList->Clear(); + window->DrawList->Flags = (g.Style.AntiAliasedLines ? ImDrawListFlags_AntiAliasedLines : 0) | (g.Style.AntiAliasedFill ? ImDrawListFlags_AntiAliasedFill : 0); + window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); + ImRect viewport_rect(GetViewportRect()); + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) + PushClipRect(parent_window->ClipRect.Min, parent_window->ClipRect.Max, true); + else + PushClipRect(viewport_rect.Min, viewport_rect.Max, true); + + // Draw modal window background (darkens what is behind them) + if ((flags & ImGuiWindowFlags_Modal) != 0 && window == GetFrontMostModalRootWindow()) + window->DrawList->AddRectFilled(viewport_rect.Min, viewport_rect.Max, GetColorU32(ImGuiCol_ModalWindowDarkening, g.ModalWindowDarkeningRatio)); + + // Draw navigation selection/windowing rectangle background + if (g.NavWindowingTarget == window) + { + ImRect bb = window->Rect(); + bb.Expand(g.FontSize); + if (!bb.Contains(viewport_rect)) // Avoid drawing if the window covers all the viewport anyway + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha * 0.25f), g.Style.WindowRounding); + } + + // Draw window + handle manual resize + const float window_rounding = window->WindowRounding; + const float window_border_size = window->WindowBorderSize; + const bool title_bar_is_highlight = want_focus || (g.NavWindow && window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); + const ImRect title_bar_rect = window->TitleBarRect(); + if (window->Collapsed) + { + // Title bar only + float backup_border_size = style.FrameBorderSize; + g.Style.FrameBorderSize = window->WindowBorderSize; + ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); + g.Style.FrameBorderSize = backup_border_size; + } + else + { + // Window background + ImU32 bg_col = GetColorU32(GetWindowBgColorIdxFromFlags(flags)); + if (g.NextWindowData.BgAlphaCond != 0) + { + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(g.NextWindowData.BgAlphaVal) << IM_COL32_A_SHIFT); + g.NextWindowData.BgAlphaCond = 0; + } + window->DrawList->AddRectFilled(window->Pos+ImVec2(0,window->TitleBarHeight()), window->Pos+window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Bot); + + // Title bar + ImU32 title_bar_col = GetColorU32(window->Collapsed ? ImGuiCol_TitleBgCollapsed : title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawCornerFlags_Top); + + // Menu bar + if (flags & ImGuiWindowFlags_MenuBar) + { + ImRect menu_bar_rect = window->MenuBarRect(); + menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. + window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawCornerFlags_Top); + if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) + window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Scrollbars + if (window->ScrollbarX) + Scrollbar(ImGuiLayoutType_Horizontal); + if (window->ScrollbarY) + Scrollbar(ImGuiLayoutType_Vertical); + + // Render resize grips (after their input handling so we don't have a frame of latency) + if (!(flags & ImGuiWindowFlags_NoResize)) + { + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPos); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(window_border_size, grip_draw_size) : ImVec2(grip_draw_size, window_border_size))); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(grip_draw_size, window_border_size) : ImVec2(window_border_size, grip_draw_size))); + window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + window_border_size), corner.y + grip.InnerDir.y * (window_rounding + window_border_size)), window_rounding, grip.AngleMin12, grip.AngleMax12); + window->DrawList->PathFillConvex(resize_grip_col[resize_grip_n]); + } + } + + // Borders + if (window_border_size > 0.0f) + window->DrawList->AddRect(window->Pos, window->Pos+window->Size, GetColorU32(ImGuiCol_Border), window_rounding, ImDrawCornerFlags_All, window_border_size); + if (border_held != -1) + { + ImRect border = GetBorderRect(window, border_held, grip_draw_size, 0.0f); + window->DrawList->AddLine(border.Min, border.Max, GetColorU32(ImGuiCol_SeparatorActive), ImMax(1.0f, window_border_size)); + } + if (style.FrameBorderSize > 0 && !(flags & ImGuiWindowFlags_NoTitleBar)) + window->DrawList->AddLine(title_bar_rect.GetBL() + ImVec2(style.WindowBorderSize, -1), title_bar_rect.GetBR() + ImVec2(-style.WindowBorderSize,-1), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Draw navigation selection/windowing rectangle border + if (g.NavWindowingTarget == window) + { + float rounding = ImMax(window->WindowRounding, g.Style.WindowRounding); + ImRect bb = window->Rect(); + bb.Expand(g.FontSize); + if (bb.Contains(viewport_rect)) // If a window fits the entire viewport, adjust its highlight inward + { + bb.Expand(-g.FontSize - 1.0f); + rounding = window->WindowRounding; + } + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), rounding, ~0, 3.0f); + } + + // Store a backup of SizeFull which we will use next frame to decide if we need scrollbars. + window->SizeFullAtLastBegin = window->SizeFull; + + // Update ContentsRegionMax. All the variable it depends on are set above in this function. + window->ContentsRegionRect.Min.x = -window->Scroll.x + window->WindowPadding.x; + window->ContentsRegionRect.Min.y = -window->Scroll.y + window->WindowPadding.y + window->TitleBarHeight() + window->MenuBarHeight(); + window->ContentsRegionRect.Max.x = -window->Scroll.x - window->WindowPadding.x + (window->SizeContentsExplicit.x != 0.0f ? window->SizeContentsExplicit.x : (window->Size.x - window->ScrollbarSizes.x)); + window->ContentsRegionRect.Max.y = -window->Scroll.y - window->WindowPadding.y + (window->SizeContentsExplicit.y != 0.0f ? window->SizeContentsExplicit.y : (window->Size.y - window->ScrollbarSizes.y)); + + // Setup drawing context + // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) + window->DC.IndentX = 0.0f + window->WindowPadding.x - window->Scroll.x; + window->DC.GroupOffsetX = 0.0f; + window->DC.ColumnsOffsetX = 0.0f; + window->DC.CursorStartPos = window->Pos + ImVec2(window->DC.IndentX + window->DC.ColumnsOffsetX, window->TitleBarHeight() + window->MenuBarHeight() + window->WindowPadding.y - window->Scroll.y); + window->DC.CursorPos = window->DC.CursorStartPos; + window->DC.CursorPosPrevLine = window->DC.CursorPos; + window->DC.CursorMaxPos = window->DC.CursorStartPos; + window->DC.CurrentLineHeight = window->DC.PrevLineHeight = 0.0f; + window->DC.CurrentLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.NavHideHighlightOneFrame = false; + window->DC.NavHasScroll = (GetScrollMaxY() > 0.0f); + window->DC.NavLayerActiveMask = window->DC.NavLayerActiveMaskNext; + window->DC.NavLayerActiveMaskNext = 0x00; + window->DC.MenuBarAppending = false; + window->DC.MenuBarOffsetX = ImMax(window->WindowPadding.x, style.ItemSpacing.x); + window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; + window->DC.ChildWindows.resize(0); + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; + window->DC.ItemFlags = ImGuiItemFlags_Default_; + window->DC.ItemWidth = window->ItemWidthDefault; + window->DC.TextWrapPos = -1.0f; // disabled + window->DC.ItemFlagsStack.resize(0); + window->DC.ItemWidthStack.resize(0); + window->DC.TextWrapPosStack.resize(0); + window->DC.ColumnsSet = NULL; + window->DC.TreeDepth = 0; + window->DC.TreeDepthMayJumpToParentOnPop = 0x00; + window->DC.StateStorage = &window->StateStorage; + window->DC.GroupStack.resize(0); + window->MenuColumns.Update(3, style.ItemSpacing.x, window_just_activated_by_user); + + if ((flags & ImGuiWindowFlags_ChildWindow) && (window->DC.ItemFlags != parent_window->DC.ItemFlags)) + { + window->DC.ItemFlags = parent_window->DC.ItemFlags; + window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); + } + + if (window->AutoFitFramesX > 0) + window->AutoFitFramesX--; + if (window->AutoFitFramesY > 0) + window->AutoFitFramesY--; + + // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) + if (want_focus) + { + FocusWindow(window); + NavInitWindow(window, false); + } + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + { + // Close & collapse button are on layer 1 (same as menus) and don't default focus + const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; + window->DC.ItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; + window->DC.NavLayerCurrent++; + window->DC.NavLayerCurrentMask <<= 1; + + // Collapse button + if (!(flags & ImGuiWindowFlags_NoCollapse)) + { + ImGuiID id = window->GetID("#COLLAPSE"); + ImRect bb(window->Pos + style.FramePadding + ImVec2(1,1), window->Pos + style.FramePadding + ImVec2(g.FontSize,g.FontSize) - ImVec2(1,1)); + ItemAdd(bb, id); // To allow navigation + if (ButtonBehavior(bb, id, NULL, NULL)) + window->CollapseToggleWanted = true; // Defer collapsing to next frame as we are too far in the Begin() function + RenderNavHighlight(bb, id); + RenderTriangle(window->Pos + style.FramePadding, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); + } + + // Close button + if (p_open != NULL) + { + const float PAD = 2.0f; + const float rad = (window->TitleBarHeight() - PAD*2.0f) * 0.5f; + if (CloseButton(window->GetID("#CLOSE"), window->Rect().GetTR() + ImVec2(-PAD - rad, PAD + rad), rad)) + *p_open = false; + } + + window->DC.NavLayerCurrent--; + window->DC.NavLayerCurrentMask >>= 1; + window->DC.ItemFlags = item_flags_backup; + + // Title text (FIXME: refactor text alignment facilities along with RenderText helpers) + ImVec2 text_size = CalcTextSize(name, NULL, true); + ImRect text_r = title_bar_rect; + float pad_left = (flags & ImGuiWindowFlags_NoCollapse) == 0 ? (style.FramePadding.x + g.FontSize + style.ItemInnerSpacing.x) : style.FramePadding.x; + float pad_right = (p_open != NULL) ? (style.FramePadding.x + g.FontSize + style.ItemInnerSpacing.x) : style.FramePadding.x; + if (style.WindowTitleAlign.x > 0.0f) pad_right = ImLerp(pad_right, pad_left, style.WindowTitleAlign.x); + text_r.Min.x += pad_left; + text_r.Max.x -= pad_right; + ImRect clip_rect = text_r; + clip_rect.Max.x = window->Pos.x + window->Size.x - (p_open ? title_bar_rect.GetHeight() - 3 : style.FramePadding.x); // Match the size of CloseButton() + RenderTextClipped(text_r.Min, text_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_rect); + } + + // Save clipped aabb so we can access it in constant-time in FindHoveredWindow() + window->WindowRectClipped = window->Rect(); + window->WindowRectClipped.ClipWith(window->ClipRect); + + // Pressing CTRL+C while holding on a window copy its content to the clipboard + // This works but 1. doesn't handle multiple Begin/End pairs, 2. recursing into another Begin/End pair - so we need to work that out and add better logging scope. + // Maybe we can support CTRL+C on every element? + /* + if (g.ActiveId == move_id) + if (g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_C)) + ImGui::LogToClipboard(); + */ + + // Inner rectangle + // We set this up after processing the resize grip so that our clip rectangle doesn't lag by a frame + // Note that if our window is collapsed we will end up with a null clipping rectangle which is the correct behavior. + window->InnerRect.Min.x = title_bar_rect.Min.x + window->WindowBorderSize; + window->InnerRect.Min.y = title_bar_rect.Max.y + window->MenuBarHeight() + (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); + window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->ScrollbarSizes.x - window->WindowBorderSize; + window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->ScrollbarSizes.y - window->WindowBorderSize; + //window->DrawList->AddRect(window->InnerRect.Min, window->InnerRect.Max, IM_COL32_WHITE); + + // After Begin() we fill the last item / hovered data using the title bar data. Make that a standard behavior (to allow usage of context menus on title bar only, etc.). + window->DC.LastItemId = window->MoveId; + window->DC.LastItemStatusFlags = IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0; + window->DC.LastItemRect = title_bar_rect; + } + + // Inner clipping rectangle + // Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + const float border_size = window->WindowBorderSize; + ImRect clip_rect; + clip_rect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(0.0f, ImFloor(window->WindowPadding.x*0.5f - border_size))); + clip_rect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y); + clip_rect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(0.0f, ImFloor(window->WindowPadding.x*0.5f - border_size))); + clip_rect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y); + PushClipRect(clip_rect.Min, clip_rect.Max, true); + + // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) + if (first_begin_of_the_frame) + window->WriteAccessed = false; + + window->BeginCount++; + g.NextWindowData.SizeConstraintCond = 0; + + // Child window can be out of sight and have "negative" clip windows. + // Mark them as collapsed so commands are skipped earlier (we can't manually collapse because they have no title bar). + if (flags & ImGuiWindowFlags_ChildWindow) + { + IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); + window->Collapsed = parent_window && parent_window->Collapsed; + + if (!(flags & ImGuiWindowFlags_AlwaysAutoResize) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + window->Collapsed |= (window->WindowRectClipped.Min.x >= window->WindowRectClipped.Max.x || window->WindowRectClipped.Min.y >= window->WindowRectClipped.Max.y); + + // We also hide the window from rendering because we've already added its border to the command list. + // (we could perform the check earlier in the function but it is simpler at this point) + if (window->Collapsed) + window->Active = false; + } + if (style.Alpha <= 0.0f) + window->Active = false; + + // Return false if we don't intend to display anything to allow user to perform an early out optimization + window->SkipItems = (window->Collapsed || !window->Active) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0; + return !window->SkipItems; +} + +// Old Begin() API with 5 parameters, avoid calling this version directly! Use SetNextWindowSize()/SetNextWindowBgAlpha() + Begin() instead. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +bool ImGui::Begin(const char* name, bool* p_open, const ImVec2& size_first_use, float bg_alpha_override, ImGuiWindowFlags flags) +{ + // Old API feature: we could pass the initial window size as a parameter. This was misleading because it only had an effect if the window didn't have data in the .ini file. + if (size_first_use.x != 0.0f || size_first_use.y != 0.0f) + ImGui::SetNextWindowSize(size_first_use, ImGuiCond_FirstUseEver); + + // Old API feature: override the window background alpha with a parameter. + if (bg_alpha_override >= 0.0f) + ImGui::SetNextWindowBgAlpha(bg_alpha_override); + + return ImGui::Begin(name, p_open, flags); +} +#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +void ImGui::End() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (window->DC.ColumnsSet != NULL) + EndColumns(); + PopClipRect(); // Inner window clip rectangle + + // Stop logging + if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging + LogFinish(); + + // Pop from window stack + g.CurrentWindowStack.pop_back(); + if (window->Flags & ImGuiWindowFlags_Popup) + g.CurrentPopupStack.pop_back(); + CheckStacksSize(window, false); + SetCurrentWindow(g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back()); +} + +// Vertical scrollbar +// The entire piece of code below is rather confusing because: +// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab) +// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar +// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. +void ImGui::Scrollbar(ImGuiLayoutType direction) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const bool horizontal = (direction == ImGuiLayoutType_Horizontal); + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(horizontal ? "#SCROLLX" : "#SCROLLY"); + + // Render background + bool other_scrollbar = (horizontal ? window->ScrollbarY : window->ScrollbarX); + float other_scrollbar_size_w = other_scrollbar ? style.ScrollbarSize : 0.0f; + const ImRect window_rect = window->Rect(); + const float border_size = window->WindowBorderSize; + ImRect bb = horizontal + ? ImRect(window->Pos.x + border_size, window_rect.Max.y - style.ScrollbarSize, window_rect.Max.x - other_scrollbar_size_w - border_size, window_rect.Max.y - border_size) + : ImRect(window_rect.Max.x - style.ScrollbarSize, window->Pos.y + border_size, window_rect.Max.x - border_size, window_rect.Max.y - other_scrollbar_size_w - border_size); + if (!horizontal) + bb.Min.y += window->TitleBarHeight() + ((window->Flags & ImGuiWindowFlags_MenuBar) ? window->MenuBarHeight() : 0.0f); + if (bb.GetWidth() <= 0.0f || bb.GetHeight() <= 0.0f) + return; + + int window_rounding_corners; + if (horizontal) + window_rounding_corners = ImDrawCornerFlags_BotLeft | (other_scrollbar ? 0 : ImDrawCornerFlags_BotRight); + else + window_rounding_corners = (((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) ? ImDrawCornerFlags_TopRight : 0) | (other_scrollbar ? 0 : ImDrawCornerFlags_BotRight); + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_ScrollbarBg), window->WindowRounding, window_rounding_corners); + bb.Expand(ImVec2(-ImClamp((float)(int)((bb.Max.x - bb.Min.x - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp((float)(int)((bb.Max.y - bb.Min.y - 2.0f) * 0.5f), 0.0f, 3.0f))); + + // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) + float scrollbar_size_v = horizontal ? bb.GetWidth() : bb.GetHeight(); + float scroll_v = horizontal ? window->Scroll.x : window->Scroll.y; + float win_size_avail_v = (horizontal ? window->SizeFull.x : window->SizeFull.y) - other_scrollbar_size_w; + float win_size_contents_v = horizontal ? window->SizeContents.x : window->SizeContents.y; + + // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) + // But we maintain a minimum size in pixel to allow for the user to still aim inside. + IM_ASSERT(ImMax(win_size_contents_v, win_size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. + const float win_size_v = ImMax(ImMax(win_size_contents_v, win_size_avail_v), 1.0f); + const float grab_h_pixels = ImClamp(scrollbar_size_v * (win_size_avail_v / win_size_v), style.GrabMinSize, scrollbar_size_v); + const float grab_h_norm = grab_h_pixels / scrollbar_size_v; + + // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). + bool held = false; + bool hovered = false; + const bool previously_held = (g.ActiveId == id); + ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); + + float scroll_max = ImMax(1.0f, win_size_contents_v - win_size_avail_v); + float scroll_ratio = ImSaturate(scroll_v / scroll_max); + float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + if (held && grab_h_norm < 1.0f) + { + float scrollbar_pos_v = horizontal ? bb.Min.x : bb.Min.y; + float mouse_pos_v = horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; + float* click_delta_to_grab_center_v = horizontal ? &g.ScrollbarClickDeltaToGrabCenter.x : &g.ScrollbarClickDeltaToGrabCenter.y; + + // Click position in scrollbar normalized space (0.0f->1.0f) + const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); + SetHoveredID(id); + + bool seek_absolute = false; + if (!previously_held) + { + // On initial click calculate the distance between mouse and the center of the grab + if (clicked_v_norm >= grab_v_norm && clicked_v_norm <= grab_v_norm + grab_h_norm) + { + *click_delta_to_grab_center_v = clicked_v_norm - grab_v_norm - grab_h_norm*0.5f; + } + else + { + seek_absolute = true; + *click_delta_to_grab_center_v = 0.0f; + } + } + + // Apply scroll + // It is ok to modify Scroll here because we are being called in Begin() after the calculation of SizeContents and before setting up our starting position + const float scroll_v_norm = ImSaturate((clicked_v_norm - *click_delta_to_grab_center_v - grab_h_norm*0.5f) / (1.0f - grab_h_norm)); + scroll_v = (float)(int)(0.5f + scroll_v_norm * scroll_max);//(win_size_contents_v - win_size_v)); + if (horizontal) + window->Scroll.x = scroll_v; + else + window->Scroll.y = scroll_v; + + // Update values for rendering + scroll_ratio = ImSaturate(scroll_v / scroll_max); + grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + + // Update distance to grab now that we have seeked and saturated + if (seek_absolute) + *click_delta_to_grab_center_v = clicked_v_norm - grab_v_norm - grab_h_norm*0.5f; + } + + // Render + const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab); + ImRect grab_rect; + if (horizontal) + grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImMin(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, window_rect.Max.x), bb.Max.y); + else + grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImMin(ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels, window_rect.Max.y)); + window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding); +} + +void ImGui::BringWindowToFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* current_front_window = g.Windows.back(); + if (current_front_window == window || current_front_window->RootWindow == window) + return; + for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the front most window + if (g.Windows[i] == window) + { + g.Windows.erase(g.Windows.Data + i); + g.Windows.push_back(window); + break; + } +} + +void ImGui::BringWindowToBack(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.Windows[0] == window) + return; + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i] == window) + { + memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); + g.Windows[0] = window; + break; + } +} + +// Moving window to front of display and set focus (which happens to be back of our sorted list) +void ImGui::FocusWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + if (g.NavWindow != window) + { + g.NavWindow = window; + if (window && g.NavDisableMouseHover) + g.NavMousePosDirty = true; + g.NavInitRequest = false; + g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId + g.NavIdIsAlive = false; + g.NavLayer = 0; + } + + // Passing NULL allow to disable keyboard focus + if (!window) + return; + + // Move the root window to the top of the pile + if (window->RootWindow) + window = window->RootWindow; + + // Steal focus on active widgets + if (window->Flags & ImGuiWindowFlags_Popup) // FIXME: This statement should be unnecessary. Need further testing before removing it.. + if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != window) + ClearActiveID(); + + // Bring to front + if (!(window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) + BringWindowToFront(window); +} + +void ImGui::FocusFrontMostActiveWindow(ImGuiWindow* ignore_window) +{ + ImGuiContext& g = *GImGui; + for (int i = g.Windows.Size - 1; i >= 0; i--) + if (g.Windows[i] != ignore_window && g.Windows[i]->WasActive && !(g.Windows[i]->Flags & ImGuiWindowFlags_ChildWindow)) + { + ImGuiWindow* focus_window = NavRestoreLastChildNavWindow(g.Windows[i]); + FocusWindow(focus_window); + return; + } +} + +void ImGui::PushItemWidth(float item_width) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); +} + +void ImGui::PushMultiItemsWidths(int components, float w_full) +{ + ImGuiWindow* window = GetCurrentWindow(); + const ImGuiStyle& style = GImGui->Style; + if (w_full <= 0.0f) + w_full = CalcItemWidth(); + const float w_item_one = ImMax(1.0f, (float)(int)((w_full - (style.ItemInnerSpacing.x) * (components-1)) / (float)components)); + const float w_item_last = ImMax(1.0f, (float)(int)(w_full - (w_item_one + style.ItemInnerSpacing.x) * (components-1))); + window->DC.ItemWidthStack.push_back(w_item_last); + for (int i = 0; i < components-1; i++) + window->DC.ItemWidthStack.push_back(w_item_one); + window->DC.ItemWidth = window->DC.ItemWidthStack.back(); +} + +void ImGui::PopItemWidth() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.ItemWidthStack.pop_back(); + window->DC.ItemWidth = window->DC.ItemWidthStack.empty() ? window->ItemWidthDefault : window->DC.ItemWidthStack.back(); +} + +float ImGui::CalcItemWidth() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + float w = window->DC.ItemWidth; + if (w < 0.0f) + { + // Align to a right-side limit. We include 1 frame padding in the calculation because this is how the width is always used (we add 2 frame padding to it), but we could move that responsibility to the widget as well. + float width_to_right_edge = GetContentRegionAvail().x; + w = ImMax(1.0f, width_to_right_edge + w); + } + w = (float)(int)w; + return w; +} + +static ImFont* GetDefaultFont() +{ + ImGuiContext& g = *GImGui; + return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; +} + +void ImGui::SetCurrentFont(ImFont* font) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(font && font->IsLoaded()); // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ? + IM_ASSERT(font->Scale > 0.0f); + g.Font = font; + g.FontBaseSize = g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale; + g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f; + + ImFontAtlas* atlas = g.Font->ContainerAtlas; + g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; + g.DrawListSharedData.Font = g.Font; + g.DrawListSharedData.FontSize = g.FontSize; +} + +void ImGui::PushFont(ImFont* font) +{ + ImGuiContext& g = *GImGui; + if (!font) + font = GetDefaultFont(); + SetCurrentFont(font); + g.FontStack.push_back(font); + g.CurrentWindow->DrawList->PushTextureID(font->ContainerAtlas->TexID); +} + +void ImGui::PopFont() +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow->DrawList->PopTextureID(); + g.FontStack.pop_back(); + SetCurrentFont(g.FontStack.empty() ? GetDefaultFont() : g.FontStack.back()); +} + +void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (enabled) + window->DC.ItemFlags |= option; + else + window->DC.ItemFlags &= ~option; + window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); +} + +void ImGui::PopItemFlag() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.ItemFlagsStack.pop_back(); + window->DC.ItemFlags = window->DC.ItemFlagsStack.empty() ? ImGuiItemFlags_Default_ : window->DC.ItemFlagsStack.back(); +} + +void ImGui::PushAllowKeyboardFocus(bool allow_keyboard_focus) +{ + PushItemFlag(ImGuiItemFlags_AllowKeyboardFocus, allow_keyboard_focus); +} + +void ImGui::PopAllowKeyboardFocus() +{ + PopItemFlag(); +} + +void ImGui::PushButtonRepeat(bool repeat) +{ + PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); +} + +void ImGui::PopButtonRepeat() +{ + PopItemFlag(); +} + +void ImGui::PushTextWrapPos(float wrap_pos_x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPos = wrap_pos_x; + window->DC.TextWrapPosStack.push_back(wrap_pos_x); +} + +void ImGui::PopTextWrapPos() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPosStack.pop_back(); + window->DC.TextWrapPos = window->DC.TextWrapPosStack.empty() ? -1.0f : window->DC.TextWrapPosStack.back(); +} + +// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 +void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiColMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorModifiers.push_back(backup); + g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); +} + +void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) +{ + ImGuiContext& g = *GImGui; + ImGuiColMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorModifiers.push_back(backup); + g.Style.Colors[idx] = col; +} + +void ImGui::PopStyleColor(int count) +{ + ImGuiContext& g = *GImGui; + while (count > 0) + { + ImGuiColMod& backup = g.ColorModifiers.back(); + g.Style.Colors[backup.Col] = backup.BackupValue; + g.ColorModifiers.pop_back(); + count--; + } +} + +struct ImGuiStyleVarInfo +{ + ImGuiDataType Type; + ImU32 Offset; + void* GetVarPtr(ImGuiStyle* style) const { return (void*)((unsigned char*)style + Offset); } +}; + +static const ImGuiStyleVarInfo GStyleVarInfo[] = +{ + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { ImGuiDataType_Float, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { ImGuiDataType_Float2, (ImU32)IM_OFFSETOF(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign +}; + +static const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx) +{ + IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_Count_); + IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_Count_); + return &GStyleVarInfo[idx]; +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) +{ + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type == ImGuiDataType_Float) + { + ImGuiContext& g = *GImGui; + float* pvar = (float*)var_info->GetVarPtr(&g.Style); + g.StyleModifiers.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; + return; + } + IM_ASSERT(0); // Called function with wrong-type? Variable is not a float. +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) +{ + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type == ImGuiDataType_Float2) + { + ImGuiContext& g = *GImGui; + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleModifiers.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; + return; + } + IM_ASSERT(0); // Called function with wrong-type? Variable is not a ImVec2. +} + +void ImGui::PopStyleVar(int count) +{ + ImGuiContext& g = *GImGui; + while (count > 0) + { + ImGuiStyleMod& backup = g.StyleModifiers.back(); + const ImGuiStyleVarInfo* info = GetStyleVarInfo(backup.VarIdx); + if (info->Type == ImGuiDataType_Float) (*(float*)info->GetVarPtr(&g.Style)) = backup.BackupFloat[0]; + else if (info->Type == ImGuiDataType_Float2) (*(ImVec2*)info->GetVarPtr(&g.Style)) = ImVec2(backup.BackupFloat[0], backup.BackupFloat[1]); + else if (info->Type == ImGuiDataType_Int) (*(int*)info->GetVarPtr(&g.Style)) = backup.BackupInt[0]; + g.StyleModifiers.pop_back(); + count--; + } +} + +const char* ImGui::GetStyleColorName(ImGuiCol idx) +{ + // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; + switch (idx) + { + case ImGuiCol_Text: return "Text"; + case ImGuiCol_TextDisabled: return "TextDisabled"; + case ImGuiCol_WindowBg: return "WindowBg"; + case ImGuiCol_ChildBg: return "ChildBg"; + case ImGuiCol_PopupBg: return "PopupBg"; + case ImGuiCol_Border: return "Border"; + case ImGuiCol_BorderShadow: return "BorderShadow"; + case ImGuiCol_FrameBg: return "FrameBg"; + case ImGuiCol_FrameBgHovered: return "FrameBgHovered"; + case ImGuiCol_FrameBgActive: return "FrameBgActive"; + case ImGuiCol_TitleBg: return "TitleBg"; + case ImGuiCol_TitleBgActive: return "TitleBgActive"; + case ImGuiCol_TitleBgCollapsed: return "TitleBgCollapsed"; + case ImGuiCol_MenuBarBg: return "MenuBarBg"; + case ImGuiCol_ScrollbarBg: return "ScrollbarBg"; + case ImGuiCol_ScrollbarGrab: return "ScrollbarGrab"; + case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; + case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; + case ImGuiCol_CheckMark: return "CheckMark"; + case ImGuiCol_SliderGrab: return "SliderGrab"; + case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; + case ImGuiCol_Button: return "Button"; + case ImGuiCol_ButtonHovered: return "ButtonHovered"; + case ImGuiCol_ButtonActive: return "ButtonActive"; + case ImGuiCol_Header: return "Header"; + case ImGuiCol_HeaderHovered: return "HeaderHovered"; + case ImGuiCol_HeaderActive: return "HeaderActive"; + case ImGuiCol_Separator: return "Separator"; + case ImGuiCol_SeparatorHovered: return "SeparatorHovered"; + case ImGuiCol_SeparatorActive: return "SeparatorActive"; + case ImGuiCol_ResizeGrip: return "ResizeGrip"; + case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered"; + case ImGuiCol_ResizeGripActive: return "ResizeGripActive"; + case ImGuiCol_CloseButton: return "CloseButton"; + case ImGuiCol_CloseButtonHovered: return "CloseButtonHovered"; + case ImGuiCol_CloseButtonActive: return "CloseButtonActive"; + case ImGuiCol_PlotLines: return "PlotLines"; + case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; + case ImGuiCol_PlotHistogram: return "PlotHistogram"; + case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; + case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; + case ImGuiCol_ModalWindowDarkening: return "ModalWindowDarkening"; + case ImGuiCol_DragDropTarget: return "DragDropTarget"; + case ImGuiCol_NavHighlight: return "NavHighlight"; + case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; + } + IM_ASSERT(0); + return "Unknown"; +} + +bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent) +{ + if (window->RootWindow == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + window = window->ParentWindow; + } + return false; +} + +bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) +{ + IM_ASSERT((flags & ImGuiHoveredFlags_AllowWhenOverlapped) == 0); // Flags not supported by this function + ImGuiContext& g = *GImGui; + + if (flags & ImGuiHoveredFlags_AnyWindow) + { + if (g.HoveredWindow == NULL) + return false; + } + else + { + switch (flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) + { + case ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows: + if (g.HoveredRootWindow != g.CurrentWindow->RootWindow) + return false; + break; + case ImGuiHoveredFlags_RootWindow: + if (g.HoveredWindow != g.CurrentWindow->RootWindow) + return false; + break; + case ImGuiHoveredFlags_ChildWindows: + if (g.HoveredWindow == NULL || !IsWindowChildOf(g.HoveredWindow, g.CurrentWindow)) + return false; + break; + default: + if (g.HoveredWindow != g.CurrentWindow) + return false; + break; + } + } + + if (!IsWindowContentHoverable(g.HoveredRootWindow, flags)) + return false; + if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != g.HoveredWindow->MoveId) + return false; + return true; +} + +bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentWindow); // Not inside a Begin()/End() + + if (flags & ImGuiFocusedFlags_AnyWindow) + return g.NavWindow != NULL; + + switch (flags & (ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows)) + { + case ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows: + return g.NavWindow && g.NavWindow->RootWindow == g.CurrentWindow->RootWindow; + case ImGuiFocusedFlags_RootWindow: + return g.NavWindow == g.CurrentWindow->RootWindow; + case ImGuiFocusedFlags_ChildWindows: + return g.NavWindow && IsWindowChildOf(g.NavWindow, g.CurrentWindow); + default: + return g.NavWindow == g.CurrentWindow; + } +} + +// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) +bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return window->Active && window == window->RootWindowForTabbing && (!(window->Flags & ImGuiWindowFlags_NoNavFocus) || window == g.NavWindow); +} + +float ImGui::GetWindowWidth() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.x; +} + +float ImGui::GetWindowHeight() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.y; +} + +ImVec2 ImGui::GetWindowPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + return window->Pos; +} + +static void SetWindowScrollX(ImGuiWindow* window, float new_scroll_x) +{ + window->DC.CursorMaxPos.x += window->Scroll.x; // SizeContents is generally computed based on CursorMaxPos which is affected by scroll position, so we need to apply our change to it. + window->Scroll.x = new_scroll_x; + window->DC.CursorMaxPos.x -= window->Scroll.x; +} + +static void SetWindowScrollY(ImGuiWindow* window, float new_scroll_y) +{ + window->DC.CursorMaxPos.y += window->Scroll.y; // SizeContents is generally computed based on CursorMaxPos which is affected by scroll position, so we need to apply our change to it. + window->Scroll.y = new_scroll_y; + window->DC.CursorMaxPos.y -= window->Scroll.y; +} + +static void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowPosAllowFlags & cond) == 0) + return; + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + window->SetWindowPosVal = ImVec2(FLT_MAX, FLT_MAX); + + // Set + const ImVec2 old_pos = window->Pos; + window->PosFloat = pos; + window->Pos = ImFloor(pos); + window->DC.CursorPos += (window->Pos - old_pos); // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor + window->DC.CursorMaxPos += (window->Pos - old_pos); // And more importantly we need to adjust this so size calculation doesn't get affected. +} + +void ImGui::SetWindowPos(const ImVec2& pos, ImGuiCond cond) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + SetWindowPos(window, pos, cond); +} + +void ImGui::SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowPos(window, pos, cond); +} + +ImVec2 ImGui::GetWindowSize() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Size; +} + +static void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowSizeAllowFlags & cond) == 0) + return; + window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // Set + if (size.x > 0.0f) + { + window->AutoFitFramesX = 0; + window->SizeFull.x = size.x; + } + else + { + window->AutoFitFramesX = 2; + window->AutoFitOnlyGrows = false; + } + if (size.y > 0.0f) + { + window->AutoFitFramesY = 0; + window->SizeFull.y = size.y; + } + else + { + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } +} + +void ImGui::SetWindowSize(const ImVec2& size, ImGuiCond cond) +{ + SetWindowSize(GImGui->CurrentWindow, size, cond); +} + +void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowSize(window, size, cond); +} + +static void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowCollapsedAllowFlags & cond) == 0) + return; + window->SetWindowCollapsedAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // Set + window->Collapsed = collapsed; +} + +void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); +} + +bool ImGui::IsWindowCollapsed() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Collapsed; +} + +bool ImGui::IsWindowAppearing() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Appearing; +} + +void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowCollapsed(window, collapsed, cond); +} + +void ImGui::SetWindowFocus() +{ + FocusWindow(GImGui->CurrentWindow); +} + +void ImGui::SetWindowFocus(const char* name) +{ + if (name) + { + if (ImGuiWindow* window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } +} + +void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.PosVal = pos; + g.NextWindowData.PosPivotVal = pivot; + g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.SizeVal = size; + g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.SizeConstraintCond = ImGuiCond_Always; + g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max); + g.NextWindowData.SizeCallback = custom_callback; + g.NextWindowData.SizeCallbackUserData = custom_callback_user_data; +} + +void ImGui::SetNextWindowContentSize(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.ContentSizeVal = size; // In Begin() we will add the size of window decorations (title bar, menu etc.) to that to form a SizeContents value. + g.NextWindowData.ContentSizeCond = ImGuiCond_Always; +} + +void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.CollapsedVal = collapsed; + g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowFocus() +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.FocusCond = ImGuiCond_Always; // Using a Cond member for consistency (may transition all of them to single flag set for fast Clear() op) +} + +void ImGui::SetNextWindowBgAlpha(float alpha) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.BgAlphaVal = alpha; + g.NextWindowData.BgAlphaCond = ImGuiCond_Always; // Using a Cond member for consistency (may transition all of them to single flag set for fast Clear() op) +} + +// In window space (not screen space!) +ImVec2 ImGui::GetContentRegionMax() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImVec2 mx = window->ContentsRegionRect.Max; + if (window->DC.ColumnsSet) + mx.x = GetColumnOffset(window->DC.ColumnsSet->Current + 1) - window->WindowPadding.x; + return mx; +} + +ImVec2 ImGui::GetContentRegionAvail() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return GetContentRegionMax() - (window->DC.CursorPos - window->Pos); +} + +float ImGui::GetContentRegionAvailWidth() +{ + return GetContentRegionAvail().x; +} + +// In window space (not screen space!) +ImVec2 ImGui::GetWindowContentRegionMin() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Min; +} + +ImVec2 ImGui::GetWindowContentRegionMax() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Max; +} + +float ImGui::GetWindowContentRegionWidth() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Max.x - window->ContentsRegionRect.Min.x; +} + +float ImGui::GetTextLineHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize; +} + +float ImGui::GetTextLineHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.ItemSpacing.y; +} + +float ImGui::GetFrameHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f; +} + +float ImGui::GetFrameHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; +} + +ImDrawList* ImGui::GetWindowDrawList() +{ + ImGuiWindow* window = GetCurrentWindow(); + return window->DrawList; +} + +ImFont* ImGui::GetFont() +{ + return GImGui->Font; +} + +float ImGui::GetFontSize() +{ + return GImGui->FontSize; +} + +ImVec2 ImGui::GetFontTexUvWhitePixel() +{ + return GImGui->DrawListSharedData.TexUvWhitePixel; +} + +void ImGui::SetWindowFontScale(float scale) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->FontWindowScale = scale; + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); +} + +// User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. +// Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'. +ImVec2 ImGui::GetCursorPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos - window->Pos + window->Scroll; +} + +float ImGui::GetCursorPosX() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.x - window->Pos.x + window->Scroll.x; +} + +float ImGui::GetCursorPosY() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; +} + +void ImGui::SetCursorPos(const ImVec2& local_pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = window->Pos - window->Scroll + local_pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +} + +void ImGui::SetCursorPosX(float x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + x; + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); +} + +void ImGui::SetCursorPosY(float y) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.y = window->Pos.y - window->Scroll.y + y; + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); +} + +ImVec2 ImGui::GetCursorStartPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorStartPos - window->Pos; +} + +ImVec2 ImGui::GetCursorScreenPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos; +} + +void ImGui::SetCursorScreenPos(const ImVec2& screen_pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = screen_pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +} + +float ImGui::GetScrollX() +{ + return GImGui->CurrentWindow->Scroll.x; +} + +float ImGui::GetScrollY() +{ + return GImGui->CurrentWindow->Scroll.y; +} + +float ImGui::GetScrollMaxX() +{ + return GetScrollMaxX(GImGui->CurrentWindow); +} + +float ImGui::GetScrollMaxY() +{ + return GetScrollMaxY(GImGui->CurrentWindow); +} + +void ImGui::SetScrollX(float scroll_x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->ScrollTarget.x = scroll_x; + window->ScrollTargetCenterRatio.x = 0.0f; +} + +void ImGui::SetScrollY(float scroll_y) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->ScrollTarget.y = scroll_y + window->TitleBarHeight() + window->MenuBarHeight(); // title bar height canceled out when using ScrollTargetRelY + window->ScrollTargetCenterRatio.y = 0.0f; +} + +void ImGui::SetScrollFromPosY(float pos_y, float center_y_ratio) +{ + // We store a target position so centering can occur on the next frame when we are guaranteed to have a known window size + ImGuiWindow* window = GetCurrentWindow(); + IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); + window->ScrollTarget.y = (float)(int)(pos_y + window->Scroll.y); + window->ScrollTargetCenterRatio.y = center_y_ratio; + + // Minor hack to to make scrolling to top/bottom of window take account of WindowPadding, it looks more right to the user this way + if (center_y_ratio <= 0.0f && window->ScrollTarget.y <= window->WindowPadding.y) + window->ScrollTarget.y = 0.0f; + else if (center_y_ratio >= 1.0f && window->ScrollTarget.y >= window->SizeContents.y - window->WindowPadding.y + GImGui->Style.ItemSpacing.y) + window->ScrollTarget.y = window->SizeContents.y; +} + +// center_y_ratio: 0.0f top of last item, 0.5f vertical center of last item, 1.0f bottom of last item. +void ImGui::SetScrollHere(float center_y_ratio) +{ + ImGuiWindow* window = GetCurrentWindow(); + float target_y = window->DC.CursorPosPrevLine.y - window->Pos.y; // Top of last item, in window space + target_y += (window->DC.PrevLineHeight * center_y_ratio) + (GImGui->Style.ItemSpacing.y * (center_y_ratio - 0.5f) * 2.0f); // Precisely aim above, in the middle or below the last line. + SetScrollFromPosY(target_y, center_y_ratio); +} + +void ImGui::ActivateItem(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavNextActivateId = id; +} + +void ImGui::SetKeyboardFocusHere(int offset) +{ + IM_ASSERT(offset >= -1); // -1 is allowed but not below + ImGuiWindow* window = GetCurrentWindow(); + window->FocusIdxAllRequestNext = window->FocusIdxAllCounter + 1 + offset; + window->FocusIdxTabRequestNext = INT_MAX; +} + +void ImGui::SetItemDefaultFocus() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!window->Appearing) + return; + if (g.NavWindow == window->RootWindowForNav && (g.NavInitRequest || g.NavInitResultId != 0) && g.NavLayer == g.NavWindow->DC.NavLayerCurrent) + { + g.NavInitRequest = false; + g.NavInitResultId = g.NavWindow->DC.LastItemId; + g.NavInitResultRectRel = ImRect(g.NavWindow->DC.LastItemRect.Min - g.NavWindow->Pos, g.NavWindow->DC.LastItemRect.Max - g.NavWindow->Pos); + NavUpdateAnyRequestFlag(); + if (!IsItemVisible()) + SetScrollHere(); + } +} + +void ImGui::SetStateStorage(ImGuiStorage* tree) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.StateStorage = tree ? tree : &window->StateStorage; +} + +ImGuiStorage* ImGui::GetStateStorage() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.StateStorage; +} + +void ImGui::TextV(const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + TextUnformatted(g.TempBuffer, text_end); +} + +void ImGui::Text(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) +{ + PushStyleColor(ImGuiCol_Text, col); + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextColoredV(col, fmt, args); + va_end(args); +} + +void ImGui::TextDisabledV(const char* fmt, va_list args) +{ + PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextDisabled(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextDisabledV(fmt, args); + va_end(args); +} + +void ImGui::TextWrappedV(const char* fmt, va_list args) +{ + bool need_wrap = (GImGui->CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position is one ia already set + if (need_wrap) PushTextWrapPos(0.0f); + TextV(fmt, args); + if (need_wrap) PopTextWrapPos(); +} + +void ImGui::TextWrapped(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextWrappedV(fmt, args); + va_end(args); +} + +void ImGui::TextUnformatted(const char* text, const char* text_end) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + IM_ASSERT(text != NULL); + const char* text_begin = text; + if (text_end == NULL) + text_end = text + strlen(text); // FIXME-OPT + + const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrentLineTextBaseOffset); + const float wrap_pos_x = window->DC.TextWrapPos; + const bool wrap_enabled = wrap_pos_x >= 0.0f; + if (text_end - text > 2000 && !wrap_enabled) + { + // Long text! + // Perform manual coarse clipping to optimize for long multi-line text + // From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled. + // We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line. + const char* line = text; + const float line_height = GetTextLineHeight(); + const ImRect clip_rect = window->ClipRect; + ImVec2 text_size(0,0); + + if (text_pos.y <= clip_rect.Max.y) + { + ImVec2 pos = text_pos; + + // Lines to skip (can't skip when logging text) + if (!g.LogEnabled) + { + int lines_skippable = (int)((clip_rect.Min.y - text_pos.y) / line_height); + if (lines_skippable > 0) + { + int lines_skipped = 0; + while (line < text_end && lines_skipped < lines_skippable) + { + const char* line_end = strchr(line, '\n'); + if (!line_end) + line_end = text_end; + line = line_end + 1; + lines_skipped++; + } + pos.y += lines_skipped * line_height; + } + } + + // Lines to render + if (line < text_end) + { + ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); + while (line < text_end) + { + const char* line_end = strchr(line, '\n'); + if (IsClippedEx(line_rect, 0, false)) + break; + + const ImVec2 line_size = CalcTextSize(line, line_end, false); + text_size.x = ImMax(text_size.x, line_size.x); + RenderText(pos, line, line_end, false); + if (!line_end) + line_end = text_end; + line = line_end + 1; + line_rect.Min.y += line_height; + line_rect.Max.y += line_height; + pos.y += line_height; + } + + // Count remaining lines + int lines_skipped = 0; + while (line < text_end) + { + const char* line_end = strchr(line, '\n'); + if (!line_end) + line_end = text_end; + line = line_end + 1; + lines_skipped++; + } + pos.y += lines_skipped * line_height; + } + + text_size.y += (pos - text_pos).y; + } + + ImRect bb(text_pos, text_pos + text_size); + ItemSize(bb); + ItemAdd(bb, 0); + } + else + { + const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; + const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); + + // Account of baseline offset + ImRect bb(text_pos, text_pos + text_size); + ItemSize(text_size); + if (!ItemAdd(bb, 0)) + return; + + // Render (we don't hide text after ## in this end-user function) + RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); + } +} + +void ImGui::AlignTextToFramePadding() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + window->DC.CurrentLineHeight = ImMax(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y * 2); + window->DC.CurrentLineTextBaseOffset = ImMax(window->DC.CurrentLineTextBaseOffset, g.Style.FramePadding.y); +} + +// Add a label+text combo aligned to other label+value widgets +void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect value_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2)); + const ImRect total_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x : 0.0f), style.FramePadding.y*2) + label_size); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, 0)) + return; + + // Render + const char* value_text_begin = &g.TempBuffer[0]; + const char* value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + RenderTextClipped(value_bb.Min, value_bb.Max, value_text_begin, value_text_end, NULL, ImVec2(0.0f,0.5f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); +} + +void ImGui::LabelText(const char* label, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LabelTextV(label, fmt, args); + va_end(args); +} + +bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + if (flags & ImGuiButtonFlags_Disabled) + { + if (out_hovered) *out_hovered = false; + if (out_held) *out_held = false; + if (g.ActiveId == id) ClearActiveID(); + return false; + } + + // Default behavior requires click+release on same spot + if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0) + flags |= ImGuiButtonFlags_PressedOnClickRelease; + + ImGuiWindow* backup_hovered_window = g.HoveredWindow; + if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) + g.HoveredWindow = window; + + bool pressed = false; + bool hovered = ItemHoverable(bb, id); + + // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button + if ((flags & ImGuiButtonFlags_PressedOnDragDropHold) && g.DragDropActive && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) + if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + { + hovered = true; + SetHoveredID(id); + if (CalcTypematicPressedRepeatAmount(g.HoveredIdTimer + 0.0001f, g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, 0.01f, 0.70f)) // FIXME: Our formula for CalcTypematicPressedRepeatAmount() is fishy + { + pressed = true; + FocusWindow(window); + } + } + + if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) + g.HoveredWindow = backup_hovered_window; + + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. + if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) + hovered = false; + + // Mouse + if (hovered) + { + if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) + { + // | CLICKING | HOLDING with ImGuiButtonFlags_Repeat + // PressedOnClickRelease | * | .. (NOT on release) <-- MOST COMMON! (*) only if both click/release were over bounds + // PressedOnClick | | .. + // PressedOnRelease | | .. (NOT on release) + // PressedOnDoubleClick | | .. + // FIXME-NAV: We don't honor those different behaviors. + if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0]) + { + SetActiveID(id, window); + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + FocusWindow(window); + } + if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0])) + { + pressed = true; + if (flags & ImGuiButtonFlags_NoHoldingActiveID) + ClearActiveID(); + else + SetActiveID(id, window); // Hold on ID + FocusWindow(window); + } + if ((flags & ImGuiButtonFlags_PressedOnRelease) && g.IO.MouseReleased[0]) + { + if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps + pressed = true; + ClearActiveID(); + } + + // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). + // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. + if ((flags & ImGuiButtonFlags_Repeat) && g.ActiveId == id && g.IO.MouseDownDuration[0] > 0.0f && IsMouseClicked(0, true)) + pressed = true; + } + + if (pressed) + g.NavDisableHighlight = true; + } + + // Gamepad/Keyboard navigation + // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. + if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) + hovered = true; + + if (g.NavActivateDownId == id) + { + bool nav_activated_by_code = (g.NavActivateId == id); + bool nav_activated_by_inputs = IsNavInputPressed(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed); + if (nav_activated_by_code || nav_activated_by_inputs) + pressed = true; + if (nav_activated_by_code || nav_activated_by_inputs || g.ActiveId == id) + { + // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. + g.NavActivateId = id; // This is so SetActiveId assign a Nav source + SetActiveID(id, window); + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + } + } + + bool held = false; + if (g.ActiveId == id) + { + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (g.ActiveIdIsJustActivated) + g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; + if (g.IO.MouseDown[0]) + { + held = true; + } + else + { + if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease)) + if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps + if (!g.DragDropActive) + pressed = true; + ClearActiveID(); + } + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + g.NavDisableHighlight = true; + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + if (g.NavActivateDownId != id) + ClearActiveID(); + } + } + + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + + return pressed; +} + +bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + ImVec2 pos = window->DC.CursorPos; + if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrentLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) + pos.y += window->DC.CurrentLineTextBaseOffset - style.FramePadding.y; + ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + size); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return false; + + if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) + flags |= ImGuiButtonFlags_Repeat; + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); + + // Automatically close popups + //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) + // CloseCurrentPopup(); + + return pressed; +} + +bool ImGui::Button(const char* label, const ImVec2& size_arg) +{ + return ButtonEx(label, size_arg, 0); +} + +// Small buttons fits within text without additional vertical spacing. +bool ImGui::SmallButton(const char* label) +{ + ImGuiContext& g = *GImGui; + float backup_padding_y = g.Style.FramePadding.y; + g.Style.FramePadding.y = 0.0f; + bool pressed = ButtonEx(label, ImVec2(0,0), ImGuiButtonFlags_AlignTextBaseLine); + g.Style.FramePadding.y = backup_padding_y; + return pressed; +} + +// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. +// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) +bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiID id = window->GetID(str_id); + ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + return pressed; +} + +// Button to close a window +bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos, float radius) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // We intentionally allow interaction when clipped so that a mechanical Alt,Right,Validate sequence close a window. + // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible). + const ImRect bb(pos - ImVec2(radius,radius), pos + ImVec2(radius,radius)); + bool is_clipped = !ItemAdd(bb, id); + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + if (is_clipped) + return pressed; + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_CloseButtonActive : hovered ? ImGuiCol_CloseButtonHovered : ImGuiCol_CloseButton); + ImVec2 center = bb.GetCenter(); + window->DrawList->AddCircleFilled(center, ImMax(2.0f, radius), col, 12); + + const float cross_extent = (radius * 0.7071f) - 1.0f; + if (hovered) + { + center -= ImVec2(0.5f, 0.5f); + window->DrawList->AddLine(center + ImVec2(+cross_extent,+cross_extent), center + ImVec2(-cross_extent,-cross_extent), GetColorU32(ImGuiCol_Text)); + window->DrawList->AddLine(center + ImVec2(+cross_extent,-cross_extent), center + ImVec2(-cross_extent,+cross_extent), GetColorU32(ImGuiCol_Text)); + } + return pressed; +} + +// [Internal] +bool ImGui::ArrowButton(ImGuiID id, ImGuiDir dir, ImVec2 padding, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + padding.x * 2.0f, g.FontSize + padding.y * 2.0f)); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + RenderTriangle(bb.Min + padding, dir, 1.0f); + + return pressed; +} + +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + if (border_col.w > 0.0f) + bb.Max += ImVec2(2,2); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + return; + + if (border_col.w > 0.0f) + { + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); + window->DrawList->AddImage(user_texture_id, bb.Min+ImVec2(1,1), bb.Max-ImVec2(1,1), uv0, uv1, GetColorU32(tint_col)); + } + else + { + window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); + } +} + +// frame_padding < 0: uses FramePadding from style (default) +// frame_padding = 0: no framing +// frame_padding > 0: set framing size +// The color used are the button colors. +bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + // Default to using texture ID as ID. User can still push string/integer prefixes. + // We could hash the size/uv to create a unique ID but that would prevent the user from animating UV. + PushID((void *)user_texture_id); + const ImGuiID id = window->GetID("#image"); + PopID(); + + const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float)frame_padding, (float)frame_padding) : style.FramePadding; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding*2); + const ImRect image_bb(window->DC.CursorPos + padding, window->DC.CursorPos + padding + size); + ItemSize(bb); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + // Render + const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, style.FrameRounding)); + if (bg_col.w > 0.0f) + window->DrawList->AddRectFilled(image_bb.Min, image_bb.Max, GetColorU32(bg_col)); + window->DrawList->AddImage(user_texture_id, image_bb.Min, image_bb.Max, uv0, uv1, GetColorU32(tint_col)); + + return pressed; +} + +// Start logging ImGui output to TTY +void ImGui::LogToTTY(int max_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.LogFile == NULL); + g.LogFile = stdout; + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; +} + +// Start logging ImGui output to given file +void ImGui::LogToFile(int max_depth, const char* filename) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + ImGuiWindow* window = g.CurrentWindow; + + if (!filename) + { + filename = g.IO.LogFilename; + if (!filename) + return; + } + + IM_ASSERT(g.LogFile == NULL); + g.LogFile = ImFileOpen(filename, "ab"); + if (!g.LogFile) + { + IM_ASSERT(g.LogFile != NULL); // Consider this an error + return; + } + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; +} + +// Start logging ImGui output to clipboard +void ImGui::LogToClipboard(int max_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.LogFile == NULL); + g.LogFile = NULL; + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; +} + +void ImGui::LogFinish() +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogText(IM_NEWLINE); + if (g.LogFile != NULL) + { + if (g.LogFile == stdout) + fflush(g.LogFile); + else + fclose(g.LogFile); + g.LogFile = NULL; + } + if (g.LogClipboard->size() > 1) + { + SetClipboardText(g.LogClipboard->begin()); + g.LogClipboard->clear(); + } + g.LogEnabled = false; +} + +// Helper to display logging buttons +void ImGui::LogButtons() +{ + ImGuiContext& g = *GImGui; + + PushID("LogButtons"); + const bool log_to_tty = Button("Log To TTY"); SameLine(); + const bool log_to_file = Button("Log To File"); SameLine(); + const bool log_to_clipboard = Button("Log To Clipboard"); SameLine(); + PushItemWidth(80.0f); + PushAllowKeyboardFocus(false); + SliderInt("Depth", &g.LogAutoExpandMaxDepth, 0, 9, NULL); + PopAllowKeyboardFocus(); + PopItemWidth(); + PopID(); + + // Start logging at the end of the function so that the buttons don't appear in the log + if (log_to_tty) + LogToTTY(g.LogAutoExpandMaxDepth); + if (log_to_file) + LogToFile(g.LogAutoExpandMaxDepth, g.IO.LogFilename); + if (log_to_clipboard) + LogToClipboard(g.LogAutoExpandMaxDepth); +} + +bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) +{ + if (flags & ImGuiTreeNodeFlags_Leaf) + return true; + + // We only write to the tree storage if the user clicks (or explicitely use SetNextTreeNode*** functions) + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStorage* storage = window->DC.StateStorage; + + bool is_open; + if (g.NextTreeNodeOpenCond != 0) + { + if (g.NextTreeNodeOpenCond & ImGuiCond_Always) + { + is_open = g.NextTreeNodeOpenVal; + storage->SetInt(id, is_open); + } + else + { + // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. + const int stored_value = storage->GetInt(id, -1); + if (stored_value == -1) + { + is_open = g.NextTreeNodeOpenVal; + storage->SetInt(id, is_open); + } + else + { + is_open = stored_value != 0; + } + } + g.NextTreeNodeOpenCond = 0; + } + else + { + is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; + } + + // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). + // NB- If we are above max depth we still allow manually opened nodes to be logged. + if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && window->DC.TreeDepth < g.LogAutoExpandMaxDepth) + is_open = true; + + return is_open; +} + +bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, 0.0f); + + if (!label_end) + label_end = FindRenderedTextEnd(label); + const ImVec2 label_size = CalcTextSize(label, label_end, false); + + // We vertically grow up to current line height up the typical widget height. + const float text_base_offset_y = ImMax(padding.y, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it + const float frame_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + style.FramePadding.y*2), label_size.y + padding.y*2); + ImRect frame_bb = ImRect(window->DC.CursorPos, ImVec2(window->Pos.x + GetContentRegionMax().x, window->DC.CursorPos.y + frame_height)); + if (display_frame) + { + // Framed header expand a little outside the default padding + frame_bb.Min.x -= (float)(int)(window->WindowPadding.x*0.5f) - 1; + frame_bb.Max.x += (float)(int)(window->WindowPadding.x*0.5f) - 1; + } + + const float text_offset_x = (g.FontSize + (display_frame ? padding.x*3 : padding.x*2)); // Collapser arrow width + Spacing + const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x*2 : 0.0f); // Include collapser + ItemSize(ImVec2(text_width, frame_height), text_base_offset_y); + + // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing + // (Ideally we'd want to add a flag for the user to specify if we want the hit test to be done up to the right side of the content or not) + const ImRect interact_bb = display_frame ? frame_bb : ImRect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + text_width + style.ItemSpacing.x*2, frame_bb.Max.y); + bool is_open = TreeNodeBehaviorIsOpen(id, flags); + + // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. + if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + window->DC.TreeDepthMayJumpToParentOnPop |= (1 << window->DC.TreeDepth); + + bool item_add = ItemAdd(interact_bb, id); + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; + window->DC.LastItemDisplayRect = frame_bb; + + if (!item_add) + { + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushRawID(id); + return is_open; + } + + // Flags that affects opening behavior: + // - 0(default) ..................... single-click anywhere to open + // - OpenOnDoubleClick .............. double-click anywhere to open + // - OpenOnArrow .................... single-click on arrow to open + // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open + ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers | ((flags & ImGuiTreeNodeFlags_AllowItemOverlap) ? ImGuiButtonFlags_AllowItemOverlap : 0); + if (!(flags & ImGuiTreeNodeFlags_Leaf)) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); + + bool hovered, held, pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); + if (!(flags & ImGuiTreeNodeFlags_Leaf)) + { + bool toggled = false; + if (pressed) + { + toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); + if (flags & ImGuiTreeNodeFlags_OpenOnArrow) + toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); + if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + toggled |= g.IO.MouseDoubleClicked[0]; + if (g.DragDropActive && is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. + toggled = false; + } + + if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open) + { + toggled = true; + NavMoveRequestCancel(); + } + if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? + { + toggled = true; + NavMoveRequestCancel(); + } + + if (toggled) + { + is_open = !is_open; + window->DC.StateStorage->SetInt(id, is_open); + } + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) + SetItemAllowOverlap(); + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y); + if (display_frame) + { + // Framed type + RenderFrame(frame_bb.Min, frame_bb.Max, col, true, style.FrameRounding); + RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); + RenderTriangle(frame_bb.Min + ImVec2(padding.x, text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); + if (g.LogEnabled) + { + // NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here. + const char log_prefix[] = "\n##"; + const char log_suffix[] = "##"; + LogRenderedText(&text_pos, log_prefix, log_prefix+3); + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + LogRenderedText(&text_pos, log_suffix+1, log_suffix+3); + } + else + { + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + } + } + else + { + // Unframed typed for tree nodes + if (hovered || (flags & ImGuiTreeNodeFlags_Selected)) + { + RenderFrame(frame_bb.Min, frame_bb.Max, col, false); + RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); + } + + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize*0.50f + text_base_offset_y)); + else if (!(flags & ImGuiTreeNodeFlags_Leaf)) + RenderTriangle(frame_bb.Min + ImVec2(padding.x, g.FontSize*0.15f + text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); + if (g.LogEnabled) + LogRenderedText(&text_pos, ">"); + RenderText(text_pos, label, label_end, false); + } + + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushRawID(id); + return is_open; +} + +// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). +// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode(). +bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen, label); +} + +bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + if (p_open && !*p_open) + return false; + + ImGuiID id = window->GetID(label); + bool is_open = TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap : 0), label); + if (p_open) + { + // Create a small overlapping close button // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. + ImGuiContext& g = *GImGui; + float button_sz = g.FontSize * 0.5f; + ImGuiItemHoveredDataBackup last_item_backup; + if (CloseButton(window->GetID((void*)(intptr_t)(id+1)), ImVec2(ImMin(window->DC.LastItemRect.Max.x, window->ClipRect.Max.x) - g.Style.FramePadding.x - button_sz, window->DC.LastItemRect.Min.y + g.Style.FramePadding.y + button_sz), button_sz)) + *p_open = false; + last_item_backup.Restore(); + } + + return is_open; +} + +bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeBehavior(window->GetID(label), flags, label, NULL); +} + +bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + return TreeNodeBehavior(window->GetID(str_id), flags, g.TempBuffer, label_end); +} + +bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + return TreeNodeBehavior(window->GetID(ptr_id), flags, g.TempBuffer, label_end); +} + +bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) +{ + return TreeNodeExV(str_id, 0, fmt, args); +} + +bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) +{ + return TreeNodeExV(ptr_id, 0, fmt, args); +} + +bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, flags, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const char* str_id, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, 0, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + return TreeNodeBehavior(window->GetID(label), 0, label, NULL); +} + +void ImGui::TreeAdvanceToLabelPos() +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow->DC.CursorPos.x += GetTreeNodeToLabelSpacing(); +} + +// Horizontal distance preceding label when using TreeNode() or Bullet() +float ImGui::GetTreeNodeToLabelSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + (g.Style.FramePadding.x * 2.0f); +} + +void ImGui::SetNextTreeNodeOpen(bool is_open, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + if (g.CurrentWindow->SkipItems) + return; + g.NextTreeNodeOpenVal = is_open; + g.NextTreeNodeOpenCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::PushID(const char* str_id) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(str_id)); +} + +void ImGui::PushID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(str_id_begin, str_id_end)); +} + +void ImGui::PushID(const void* ptr_id) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(ptr_id)); +} + +void ImGui::PushID(int int_id) +{ + const void* ptr_id = (void*)(intptr_t)int_id; + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(ptr_id)); +} + +void ImGui::PopID() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.pop_back(); +} + +ImGuiID ImGui::GetID(const char* str_id) +{ + return GImGui->CurrentWindow->GetID(str_id); +} + +ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) +{ + return GImGui->CurrentWindow->GetID(str_id_begin, str_id_end); +} + +ImGuiID ImGui::GetID(const void* ptr_id) +{ + return GImGui->CurrentWindow->GetID(ptr_id); +} + +void ImGui::Bullet() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float line_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y*2), g.FontSize); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + { + SameLine(0, style.FramePadding.x*2); + return; + } + + // Render and stay on same line + RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f)); + SameLine(0, style.FramePadding.x*2); +} + +// Text with a little bullet aligned to the typical tree node. +void ImGui::BulletTextV(const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + const char* text_begin = g.TempBuffer; + const char* text_end = text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); + const float text_base_offset_y = ImMax(0.0f, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it + const float line_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y*2), g.FontSize); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x*2) : 0.0f), ImMax(line_height, label_size.y))); // Empty text doesn't add padding + ItemSize(bb); + if (!ItemAdd(bb, 0)) + return; + + // Render + RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f)); + RenderText(bb.Min+ImVec2(g.FontSize + style.FramePadding.x*2, text_base_offset_y), text_begin, text_end, false); +} + +void ImGui::BulletText(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + BulletTextV(fmt, args); + va_end(args); +} + +static inline void DataTypeFormatString(ImGuiDataType data_type, void* data_ptr, const char* display_format, char* buf, int buf_size) +{ + if (data_type == ImGuiDataType_Int) + ImFormatString(buf, buf_size, display_format, *(int*)data_ptr); + else if (data_type == ImGuiDataType_Float) + ImFormatString(buf, buf_size, display_format, *(float*)data_ptr); +} + +static inline void DataTypeFormatString(ImGuiDataType data_type, void* data_ptr, int decimal_precision, char* buf, int buf_size) +{ + if (data_type == ImGuiDataType_Int) + { + if (decimal_precision < 0) + ImFormatString(buf, buf_size, "%d", *(int*)data_ptr); + else + ImFormatString(buf, buf_size, "%.*d", decimal_precision, *(int*)data_ptr); + } + else if (data_type == ImGuiDataType_Float) + { + if (decimal_precision < 0) + ImFormatString(buf, buf_size, "%f", *(float*)data_ptr); // Ideally we'd have a minimum decimal precision of 1 to visually denote that it is a float, while hiding non-significant digits? + else + ImFormatString(buf, buf_size, "%.*f", decimal_precision, *(float*)data_ptr); + } +} + +static void DataTypeApplyOp(ImGuiDataType data_type, int op, void* value1, const void* value2)// Store into value1 +{ + if (data_type == ImGuiDataType_Int) + { + if (op == '+') + *(int*)value1 = *(int*)value1 + *(const int*)value2; + else if (op == '-') + *(int*)value1 = *(int*)value1 - *(const int*)value2; + } + else if (data_type == ImGuiDataType_Float) + { + if (op == '+') + *(float*)value1 = *(float*)value1 + *(const float*)value2; + else if (op == '-') + *(float*)value1 = *(float*)value1 - *(const float*)value2; + } +} + +// User can input math operators (e.g. +100) to edit a numerical values. +static bool DataTypeApplyOpFromText(const char* buf, const char* initial_value_buf, ImGuiDataType data_type, void* data_ptr, const char* scalar_format) +{ + while (ImCharIsSpace(*buf)) + buf++; + + // We don't support '-' op because it would conflict with inputing negative value. + // Instead you can use +-100 to subtract from an existing value + char op = buf[0]; + if (op == '+' || op == '*' || op == '/') + { + buf++; + while (ImCharIsSpace(*buf)) + buf++; + } + else + { + op = 0; + } + if (!buf[0]) + return false; + + if (data_type == ImGuiDataType_Int) + { + if (!scalar_format) + scalar_format = "%d"; + int* v = (int*)data_ptr; + const int old_v = *v; + int arg0i = *v; + if (op && sscanf(initial_value_buf, scalar_format, &arg0i) < 1) + return false; + + // Store operand in a float so we can use fractional value for multipliers (*1.1), but constant always parsed as integer so we can fit big integers (e.g. 2000000003) past float precision + float arg1f = 0.0f; + if (op == '+') { if (sscanf(buf, "%f", &arg1f) == 1) *v = (int)(arg0i + arg1f); } // Add (use "+-" to subtract) + else if (op == '*') { if (sscanf(buf, "%f", &arg1f) == 1) *v = (int)(arg0i * arg1f); } // Multiply + else if (op == '/') { if (sscanf(buf, "%f", &arg1f) == 1 && arg1f != 0.0f) *v = (int)(arg0i / arg1f); }// Divide + else { if (sscanf(buf, scalar_format, &arg0i) == 1) *v = arg0i; } // Assign constant (read as integer so big values are not lossy) + return (old_v != *v); + } + else if (data_type == ImGuiDataType_Float) + { + // For floats we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in + scalar_format = "%f"; + float* v = (float*)data_ptr; + const float old_v = *v; + float arg0f = *v; + if (op && sscanf(initial_value_buf, scalar_format, &arg0f) < 1) + return false; + + float arg1f = 0.0f; + if (sscanf(buf, scalar_format, &arg1f) < 1) + return false; + if (op == '+') { *v = arg0f + arg1f; } // Add (use "+-" to subtract) + else if (op == '*') { *v = arg0f * arg1f; } // Multiply + else if (op == '/') { if (arg1f != 0.0f) *v = arg0f / arg1f; } // Divide + else { *v = arg1f; } // Assign constant + return (old_v != *v); + } + + return false; +} + +// Create text input in place of a slider (when CTRL+Clicking on slider) +// FIXME: Logic is messy and confusing. +bool ImGui::InputScalarAsWidgetReplacement(const ImRect& aabb, const char* label, ImGuiDataType data_type, void* data_ptr, ImGuiID id, int decimal_precision) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + // Our replacement widget will override the focus ID (registered previously to allow for a TAB focus to happen) + // On the first frame, g.ScalarAsInputTextId == 0, then on subsequent frames it becomes == id + SetActiveID(g.ScalarAsInputTextId, window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + SetHoveredID(0); + FocusableItemUnregister(window); + + char buf[32]; + DataTypeFormatString(data_type, data_ptr, decimal_precision, buf, IM_ARRAYSIZE(buf)); + bool text_value_changed = InputTextEx(label, buf, IM_ARRAYSIZE(buf), aabb.GetSize(), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll); + if (g.ScalarAsInputTextId == 0) // First frame we started displaying the InputText widget + { + IM_ASSERT(g.ActiveId == id); // InputText ID expected to match the Slider ID (else we'd need to store them both, which is also possible) + g.ScalarAsInputTextId = g.ActiveId; + SetHoveredID(id); + } + if (text_value_changed) + return DataTypeApplyOpFromText(buf, GImGui->InputTextState.InitialText.begin(), data_type, data_ptr, NULL); + return false; +} + +// Parse display precision back from the display format string +int ImGui::ParseFormatPrecision(const char* fmt, int default_precision) +{ + int precision = default_precision; + while ((fmt = strchr(fmt, '%')) != NULL) + { + fmt++; + if (fmt[0] == '%') { fmt++; continue; } // Ignore "%%" + while (*fmt >= '0' && *fmt <= '9') + fmt++; + if (*fmt == '.') + { + fmt = ImAtoi(fmt + 1, &precision); + if (precision < 0 || precision > 10) + precision = default_precision; + } + if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation + precision = -1; + break; + } + return precision; +} + +static float GetMinimumStepAtDecimalPrecision(int decimal_precision) +{ + static const float min_steps[10] = { 1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f }; + return (decimal_precision >= 0 && decimal_precision < 10) ? min_steps[decimal_precision] : powf(10.0f, (float)-decimal_precision); +} + +float ImGui::RoundScalar(float value, int decimal_precision) +{ + // Round past decimal precision + // So when our value is 1.99999 with a precision of 0.001 we'll end up rounding to 2.0 + // FIXME: Investigate better rounding methods + if (decimal_precision < 0) + return value; + const float min_step = GetMinimumStepAtDecimalPrecision(decimal_precision); + bool negative = value < 0.0f; + value = fabsf(value); + float remainder = fmodf(value, min_step); + if (remainder <= min_step*0.5f) + value -= remainder; + else + value += (min_step - remainder); + return negative ? -value : value; +} + +static inline float SliderBehaviorCalcRatioFromValue(float v, float v_min, float v_max, float power, float linear_zero_pos) +{ + if (v_min == v_max) + return 0.0f; + + const bool is_non_linear = (power < 1.0f-0.00001f) || (power > 1.0f+0.00001f); + const float v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); + if (is_non_linear) + { + if (v_clamped < 0.0f) + { + const float f = 1.0f - (v_clamped - v_min) / (ImMin(0.0f,v_max) - v_min); + return (1.0f - powf(f, 1.0f/power)) * linear_zero_pos; + } + else + { + const float f = (v_clamped - ImMax(0.0f,v_min)) / (v_max - ImMax(0.0f,v_min)); + return linear_zero_pos + powf(f, 1.0f/power) * (1.0f - linear_zero_pos); + } + } + + // Linear slider + return (v_clamped - v_min) / (v_max - v_min); +} + +bool ImGui::SliderBehavior(const ImRect& frame_bb, ImGuiID id, float* v, float v_min, float v_max, float power, int decimal_precision, ImGuiSliderFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + const ImGuiStyle& style = g.Style; + + // Draw frame + const ImU32 frame_col = GetColorU32((g.ActiveId == id && g.ActiveIdSource == ImGuiInputSource_Nav) ? ImGuiCol_FrameBgActive : ImGuiCol_FrameBg); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); + + const bool is_non_linear = (power < 1.0f-0.00001f) || (power > 1.0f+0.00001f); + const bool is_horizontal = (flags & ImGuiSliderFlags_Vertical) == 0; + + const float grab_padding = 2.0f; + const float slider_sz = is_horizontal ? (frame_bb.GetWidth() - grab_padding * 2.0f) : (frame_bb.GetHeight() - grab_padding * 2.0f); + float grab_sz; + if (decimal_precision != 0) + grab_sz = ImMin(style.GrabMinSize, slider_sz); + else + grab_sz = ImMin(ImMax(1.0f * (slider_sz / ((v_min < v_max ? v_max - v_min : v_min - v_max) + 1.0f)), style.GrabMinSize), slider_sz); // Integer sliders, if possible have the grab size represent 1 unit + const float slider_usable_sz = slider_sz - grab_sz; + const float slider_usable_pos_min = (is_horizontal ? frame_bb.Min.x : frame_bb.Min.y) + grab_padding + grab_sz*0.5f; + const float slider_usable_pos_max = (is_horizontal ? frame_bb.Max.x : frame_bb.Max.y) - grab_padding - grab_sz*0.5f; + + // For logarithmic sliders that cross over sign boundary we want the exponential increase to be symmetric around 0.0f + float linear_zero_pos = 0.0f; // 0.0->1.0f + if (v_min * v_max < 0.0f) + { + // Different sign + const float linear_dist_min_to_0 = powf(fabsf(0.0f - v_min), 1.0f/power); + const float linear_dist_max_to_0 = powf(fabsf(v_max - 0.0f), 1.0f/power); + linear_zero_pos = linear_dist_min_to_0 / (linear_dist_min_to_0+linear_dist_max_to_0); + } + else + { + // Same sign + linear_zero_pos = v_min < 0.0f ? 1.0f : 0.0f; + } + + // Process interacting with the slider + bool value_changed = false; + if (g.ActiveId == id) + { + bool set_new_value = false; + float clicked_t = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (!g.IO.MouseDown[0]) + { + ClearActiveID(); + } + else + { + const float mouse_abs_pos = is_horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; + clicked_t = (slider_usable_sz > 0.0f) ? ImClamp((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz, 0.0f, 1.0f) : 0.0f; + if (!is_horizontal) + clicked_t = 1.0f - clicked_t; + set_new_value = true; + } + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + const ImVec2 delta2 = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 0.0f, 0.0f); + float delta = is_horizontal ? delta2.x : -delta2.y; + if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + { + ClearActiveID(); + } + else if (delta != 0.0f) + { + clicked_t = SliderBehaviorCalcRatioFromValue(*v, v_min, v_max, power, linear_zero_pos); + if (decimal_precision == 0 && !is_non_linear) + { + if (fabsf(v_max - v_min) <= 100.0f || IsNavInputDown(ImGuiNavInput_TweakSlow)) + delta = ((delta < 0.0f) ? -1.0f : +1.0f) / (v_max - v_min); // Gamepad/keyboard tweak speeds in integer steps + else + delta /= 100.0f; + } + else + { + delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds + if (IsNavInputDown(ImGuiNavInput_TweakSlow)) + delta /= 10.0f; + } + if (IsNavInputDown(ImGuiNavInput_TweakFast)) + delta *= 10.0f; + set_new_value = true; + if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits + set_new_value = false; + else + clicked_t = ImSaturate(clicked_t + delta); + } + } + + if (set_new_value) + { + float new_value; + if (is_non_linear) + { + // Account for logarithmic scale on both sides of the zero + if (clicked_t < linear_zero_pos) + { + // Negative: rescale to the negative range before powering + float a = 1.0f - (clicked_t / linear_zero_pos); + a = powf(a, power); + new_value = ImLerp(ImMin(v_max,0.0f), v_min, a); + } + else + { + // Positive: rescale to the positive range before powering + float a; + if (fabsf(linear_zero_pos - 1.0f) > 1.e-6f) + a = (clicked_t - linear_zero_pos) / (1.0f - linear_zero_pos); + else + a = clicked_t; + a = powf(a, power); + new_value = ImLerp(ImMax(v_min,0.0f), v_max, a); + } + } + else + { + // Linear slider + new_value = ImLerp(v_min, v_max, clicked_t); + } + + // Round past decimal precision + new_value = RoundScalar(new_value, decimal_precision); + if (*v != new_value) + { + *v = new_value; + value_changed = true; + } + } + } + + // Draw + float grab_t = SliderBehaviorCalcRatioFromValue(*v, v_min, v_max, power, linear_zero_pos); + if (!is_horizontal) + grab_t = 1.0f - grab_t; + const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); + ImRect grab_bb; + if (is_horizontal) + grab_bb = ImRect(ImVec2(grab_pos - grab_sz*0.5f, frame_bb.Min.y + grab_padding), ImVec2(grab_pos + grab_sz*0.5f, frame_bb.Max.y - grab_padding)); + else + grab_bb = ImRect(ImVec2(frame_bb.Min.x + grab_padding, grab_pos - grab_sz*0.5f), ImVec2(frame_bb.Max.x - grab_padding, grab_pos + grab_sz*0.5f)); + window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); + + return value_changed; +} + +// Use power!=1.0 for logarithmic sliders. +// Adjust display_format to decorate the value with a prefix or a suffix. +// "%.3f" 1.234 +// "%5.2f secs" 01.23 secs +// "Gold: %.0f" Gold: 1 +bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + // NB- we don't call ItemSize() yet because we may turn into a text edit box below + if (!ItemAdd(total_bb, id, &frame_bb)) + { + ItemSize(total_bb, style.FramePadding.y); + return false; + } + const bool hovered = ItemHoverable(frame_bb, id); + + if (!display_format) + display_format = "%.3f"; + int decimal_precision = ParseFormatPrecision(display_format, 3); + + // Tabbing or CTRL-clicking on Slider turns it into an input box + bool start_text_input = false; + const bool tab_focus_requested = FocusableItemRegister(window, id); + if (tab_focus_requested || (hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || (g.NavInputId == id && g.ScalarAsInputTextId != id)) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + if (tab_focus_requested || g.IO.KeyCtrl || g.NavInputId == id) + { + start_text_input = true; + g.ScalarAsInputTextId = 0; + } + } + if (start_text_input || (g.ActiveId == id && g.ScalarAsInputTextId == id)) + return InputScalarAsWidgetReplacement(frame_bb, label, ImGuiDataType_Float, v, id, decimal_precision); + + // Actual slider behavior + render grab + ItemSize(total_bb, style.FramePadding.y); + const bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + return value_changed; +} + +bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); + const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(frame_bb, id)) + return false; + const bool hovered = ItemHoverable(frame_bb, id); + + if (!display_format) + display_format = "%.3f"; + int decimal_precision = ParseFormatPrecision(display_format, 3); + + if ((hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || g.NavInputId == id) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + } + + // Actual slider behavior + render grab + bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision, ImGuiSliderFlags_Vertical); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + // For the vertical slider we allow centered text to overlap the frame padding + char value_buf[64]; + char* value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.0f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + return value_changed; +} + +bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max) +{ + float v_deg = (*v_rad) * 360.0f / (2*IM_PI); + bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, "%.0f deg", 1.0f); + *v_rad = v_deg * (2*IM_PI) / 360.0f; + return value_changed; +} + +bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* display_format) +{ + if (!display_format) + display_format = "%.0f"; + float v_f = (float)*v; + bool value_changed = SliderFloat(label, &v_f, (float)v_min, (float)v_max, display_format, 1.0f); + *v = (int)v_f; + return value_changed; +} + +bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* display_format) +{ + if (!display_format) + display_format = "%.0f"; + float v_f = (float)*v; + bool value_changed = VSliderFloat(label, size, &v_f, (float)v_min, (float)v_max, display_format, 1.0f); + *v = (int)v_f; + return value_changed; +} + +// Add multiple sliders on 1 line for compact edition of multiple components +bool ImGui::SliderFloatN(const char* label, float* v, int components, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components); + for (int i = 0; i < components; i++) + { + PushID(i); + value_changed |= SliderFloat("##v", &v[i], v_min, v_max, display_format, power); + SameLine(0, g.Style.ItemInnerSpacing.x); + PopID(); + PopItemWidth(); + } + PopID(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* display_format, float power) +{ + return SliderFloatN(label, v, 2, v_min, v_max, display_format, power); +} + +bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* display_format, float power) +{ + return SliderFloatN(label, v, 3, v_min, v_max, display_format, power); +} + +bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* display_format, float power) +{ + return SliderFloatN(label, v, 4, v_min, v_max, display_format, power); +} + +bool ImGui::SliderIntN(const char* label, int* v, int components, int v_min, int v_max, const char* display_format) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components); + for (int i = 0; i < components; i++) + { + PushID(i); + value_changed |= SliderInt("##v", &v[i], v_min, v_max, display_format); + SameLine(0, g.Style.ItemInnerSpacing.x); + PopID(); + PopItemWidth(); + } + PopID(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* display_format) +{ + return SliderIntN(label, v, 2, v_min, v_max, display_format); +} + +bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* display_format) +{ + return SliderIntN(label, v, 3, v_min, v_max, display_format); +} + +bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* display_format) +{ + return SliderIntN(label, v, 4, v_min, v_max, display_format); +} + +bool ImGui::DragBehavior(const ImRect& frame_bb, ImGuiID id, float* v, float v_speed, float v_min, float v_max, int decimal_precision, float power) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); + + bool value_changed = false; + + // Process interacting with the drag + if (g.ActiveId == id) + { + if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) + ClearActiveID(); + else if (g.ActiveIdSource == ImGuiInputSource_Nav && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + ClearActiveID(); + } + if (g.ActiveId == id) + { + if (g.ActiveIdIsJustActivated) + { + // Lock current value on click + g.DragCurrentValue = *v; + g.DragLastMouseDelta = ImVec2(0.f, 0.f); + } + + if (v_speed == 0.0f && (v_max - v_min) != 0.0f && (v_max - v_min) < FLT_MAX) + v_speed = (v_max - v_min) * g.DragSpeedDefaultRatio; + + float v_cur = g.DragCurrentValue; + const ImVec2 mouse_drag_delta = GetMouseDragDelta(0, 1.0f); + float adjust_delta = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid()) + { + adjust_delta = mouse_drag_delta.x - g.DragLastMouseDelta.x; + if (g.IO.KeyShift && g.DragSpeedScaleFast >= 0.0f) + adjust_delta *= g.DragSpeedScaleFast; + if (g.IO.KeyAlt && g.DragSpeedScaleSlow >= 0.0f) + adjust_delta *= g.DragSpeedScaleSlow; + g.DragLastMouseDelta.x = mouse_drag_delta.x; + } + if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + adjust_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard|ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 1.0f/10.0f, 10.0f).x; + if (v_min < v_max && ((v_cur >= v_max && adjust_delta > 0.0f) || (v_cur <= v_min && adjust_delta < 0.0f))) // This is to avoid applying the saturation when already past the limits + adjust_delta = 0.0f; + v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); + } + adjust_delta *= v_speed; + + if (fabsf(adjust_delta) > 0.0f) + { + if (fabsf(power - 1.0f) > 0.001f) + { + // Logarithmic curve on both side of 0.0 + float v0_abs = v_cur >= 0.0f ? v_cur : -v_cur; + float v0_sign = v_cur >= 0.0f ? 1.0f : -1.0f; + float v1 = powf(v0_abs, 1.0f / power) + (adjust_delta * v0_sign); + float v1_abs = v1 >= 0.0f ? v1 : -v1; + float v1_sign = v1 >= 0.0f ? 1.0f : -1.0f; // Crossed sign line + v_cur = powf(v1_abs, power) * v0_sign * v1_sign; // Reapply sign + } + else + { + v_cur += adjust_delta; + } + + // Clamp + if (v_min < v_max) + v_cur = ImClamp(v_cur, v_min, v_max); + g.DragCurrentValue = v_cur; + } + + // Round to user desired precision, then apply + v_cur = RoundScalar(v_cur, decimal_precision); + if (*v != v_cur) + { + *v = v_cur; + value_changed = true; + } + } + + return value_changed; +} + +bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); + const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + // NB- we don't call ItemSize() yet because we may turn into a text edit box below + if (!ItemAdd(total_bb, id, &frame_bb)) + { + ItemSize(total_bb, style.FramePadding.y); + return false; + } + const bool hovered = ItemHoverable(frame_bb, id); + + if (!display_format) + display_format = "%.3f"; + int decimal_precision = ParseFormatPrecision(display_format, 3); + + // Tabbing or CTRL-clicking on Drag turns it into an input box + bool start_text_input = false; + const bool tab_focus_requested = FocusableItemRegister(window, id); + if (tab_focus_requested || (hovered && (g.IO.MouseClicked[0] || g.IO.MouseDoubleClicked[0])) || g.NavActivateId == id || (g.NavInputId == id && g.ScalarAsInputTextId != id)) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + if (tab_focus_requested || g.IO.KeyCtrl || g.IO.MouseDoubleClicked[0] || g.NavInputId == id) + { + start_text_input = true; + g.ScalarAsInputTextId = 0; + } + } + if (start_text_input || (g.ActiveId == id && g.ScalarAsInputTextId == id)) + return InputScalarAsWidgetReplacement(frame_bb, label, ImGuiDataType_Float, v, id, decimal_precision); + + // Actual drag behavior + ItemSize(total_bb, style.FramePadding.y); + const bool value_changed = DragBehavior(frame_bb, id, v, v_speed, v_min, v_max, decimal_precision, power); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); + + return value_changed; +} + +bool ImGui::DragFloatN(const char* label, float* v, int components, float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components); + for (int i = 0; i < components; i++) + { + PushID(i); + value_changed |= DragFloat("##v", &v[i], v_speed, v_min, v_max, display_format, power); + SameLine(0, g.Style.ItemInnerSpacing.x); + PopID(); + PopItemWidth(); + } + PopID(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + return DragFloatN(label, v, 2, v_speed, v_min, v_max, display_format, power); +} + +bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + return DragFloatN(label, v, 3, v_speed, v_min, v_max, display_format, power); +} + +bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + return DragFloatN(label, v, 4, v_speed, v_min, v_max, display_format, power); +} + +bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* display_format, const char* display_format_max, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2); + + bool value_changed = DragFloat("##min", v_current_min, v_speed, (v_min >= v_max) ? -FLT_MAX : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), display_format, power); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= DragFloat("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? FLT_MAX : v_max, display_format_max ? display_format_max : display_format, power); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +// NB: v_speed is float to allow adjusting the drag speed with more precision +bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* display_format) +{ + if (!display_format) + display_format = "%.0f"; + float v_f = (float)*v; + bool value_changed = DragFloat(label, &v_f, v_speed, (float)v_min, (float)v_max, display_format); + *v = (int)v_f; + return value_changed; +} + +bool ImGui::DragIntN(const char* label, int* v, int components, float v_speed, int v_min, int v_max, const char* display_format) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components); + for (int i = 0; i < components; i++) + { + PushID(i); + value_changed |= DragInt("##v", &v[i], v_speed, v_min, v_max, display_format); + SameLine(0, g.Style.ItemInnerSpacing.x); + PopID(); + PopItemWidth(); + } + PopID(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* display_format) +{ + return DragIntN(label, v, 2, v_speed, v_min, v_max, display_format); +} + +bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* display_format) +{ + return DragIntN(label, v, 3, v_speed, v_min, v_max, display_format); +} + +bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* display_format) +{ + return DragIntN(label, v, 4, v_speed, v_min, v_max, display_format); +} + +bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* display_format, const char* display_format_max) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2); + + bool value_changed = DragInt("##min", v_current_min, v_speed, (v_min >= v_max) ? INT_MIN : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), display_format); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= DragInt("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? INT_MAX : v_max, display_format_max ? display_format_max : display_format); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + if (graph_size.x == 0.0f) + graph_size.x = CalcItemWidth(); + if (graph_size.y == 0.0f) + graph_size.y = label_size.y + (style.FramePadding.y * 2); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(graph_size.x, graph_size.y)); + const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, 0, &frame_bb)) + return; + const bool hovered = ItemHoverable(inner_bb, 0); + + // Determine scale from values if not specified + if (scale_min == FLT_MAX || scale_max == FLT_MAX) + { + float v_min = FLT_MAX; + float v_max = -FLT_MAX; + for (int i = 0; i < values_count; i++) + { + const float v = values_getter(data, i); + v_min = ImMin(v_min, v); + v_max = ImMax(v_max, v); + } + if (scale_min == FLT_MAX) + scale_min = v_min; + if (scale_max == FLT_MAX) + scale_max = v_max; + } + + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + if (values_count > 0) + { + int res_w = ImMin((int)graph_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + + // Tooltip on hover + int v_hovered = -1; + if (hovered) + { + const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); + const int v_idx = (int)(t * item_count); + IM_ASSERT(v_idx >= 0 && v_idx < values_count); + + const float v0 = values_getter(data, (v_idx + values_offset) % values_count); + const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); + if (plot_type == ImGuiPlotType_Lines) + SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx+1, v1); + else if (plot_type == ImGuiPlotType_Histogram) + SetTooltip("%d: %8.4g", v_idx, v0); + v_hovered = v_idx; + } + + const float t_step = 1.0f / (float)res_w; + const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); + + float v0 = values_getter(data, (0 + values_offset) % values_count); + float t0 = 0.0f; + ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle + float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (-scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands + + const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); + const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); + + for (int n = 0; n < res_w; n++) + { + const float t1 = t0 + t_step; + const int v1_idx = (int)(t0 * item_count + 0.5f); + IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); + const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); + const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) ); + + // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. + ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); + ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); + if (plot_type == ImGuiPlotType_Lines) + { + window->DrawList->AddLine(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); + } + else if (plot_type == ImGuiPlotType_Histogram) + { + if (pos1.x >= pos0.x + 2.0f) + pos1.x -= 1.0f; + window->DrawList->AddRectFilled(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); + } + + t0 = t1; + tp0 = tp1; + } + } + + // Text overlay + if (overlay_text) + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f,0.0f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); +} + +struct ImGuiPlotArrayGetterData +{ + const float* Values; + int Stride; + + ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } +}; + +static float Plot_ArrayGetter(void* data, int idx) +{ + ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data; + const float v = *(float*)(void*)((unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride); + return v; +} + +void ImGui::PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size +void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + ImVec2 pos = window->DC.CursorPos; + ImRect bb(pos, pos + CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y*2.0f)); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, 0)) + return; + + // Render + fraction = ImSaturate(fraction); + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); + const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y); + RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding); + + // Default displaying the fraction as percentage string, but user can override it + char overlay_buf[32]; + if (!overlay) + { + ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction*100+0.01f); + overlay = overlay_buf; + } + + ImVec2 overlay_size = CalcTextSize(overlay, NULL); + if (overlay_size.x > 0.0f) + RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f,0.5f), &bb); +} + +bool ImGui::Checkbox(const char* label, bool* v) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y*2, label_size.y + style.FramePadding.y*2)); // We want a square shape to we use Y twice + ItemSize(check_bb, style.FramePadding.y); + + ImRect total_bb = check_bb; + if (label_size.x > 0) + SameLine(0, style.ItemInnerSpacing.x); + const ImRect text_bb(window->DC.CursorPos + ImVec2(0,style.FramePadding.y), window->DC.CursorPos + ImVec2(0,style.FramePadding.y) + label_size); + if (label_size.x > 0) + { + ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y); + total_bb = ImRect(ImMin(check_bb.Min, text_bb.Min), ImMax(check_bb.Max, text_bb.Max)); + } + + if (!ItemAdd(total_bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) + *v = !(*v); + + RenderNavHighlight(total_bb, id); + RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); + if (*v) + { + const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight()); + const float pad = ImMax(1.0f, (float)(int)(check_sz / 6.0f)); + RenderCheckMark(check_bb.Min + ImVec2(pad,pad), GetColorU32(ImGuiCol_CheckMark), check_bb.GetWidth() - pad*2.0f); + } + + if (g.LogEnabled) + LogRenderedText(&text_bb.Min, *v ? "[x]" : "[ ]"); + if (label_size.x > 0.0f) + RenderText(text_bb.Min, label); + + return pressed; +} + +bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) +{ + bool v = ((*flags & flags_value) == flags_value); + bool pressed = Checkbox(label, &v); + if (pressed) + { + if (v) + *flags |= flags_value; + else + *flags &= ~flags_value; + } + + return pressed; +} + +bool ImGui::RadioButton(const char* label, bool active) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y*2-1, label_size.y + style.FramePadding.y*2-1)); + ItemSize(check_bb, style.FramePadding.y); + + ImRect total_bb = check_bb; + if (label_size.x > 0) + SameLine(0, style.ItemInnerSpacing.x); + const ImRect text_bb(window->DC.CursorPos + ImVec2(0, style.FramePadding.y), window->DC.CursorPos + ImVec2(0, style.FramePadding.y) + label_size); + if (label_size.x > 0) + { + ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y); + total_bb.Add(text_bb); + } + + if (!ItemAdd(total_bb, id)) + return false; + + ImVec2 center = check_bb.GetCenter(); + center.x = (float)(int)center.x + 0.5f; + center.y = (float)(int)center.y + 0.5f; + const float radius = check_bb.GetHeight() * 0.5f; + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + + RenderNavHighlight(total_bb, id); + window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 16); + if (active) + { + const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight()); + const float pad = ImMax(1.0f, (float)(int)(check_sz / 6.0f)); + window->DrawList->AddCircleFilled(center, radius-pad, GetColorU32(ImGuiCol_CheckMark), 16); + } + + if (style.FrameBorderSize > 0.0f) + { + window->DrawList->AddCircle(center+ImVec2(1,1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize); + window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), 16, style.FrameBorderSize); + } + + if (g.LogEnabled) + LogRenderedText(&text_bb.Min, active ? "(x)" : "( )"); + if (label_size.x > 0.0f) + RenderText(text_bb.Min, label); + + return pressed; +} + +bool ImGui::RadioButton(const char* label, int* v, int v_button) +{ + const bool pressed = RadioButton(label, *v == v_button); + if (pressed) + { + *v = v_button; + } + return pressed; +} + +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) +{ + int line_count = 0; + const char* s = text_begin; + while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding + if (c == '\n') + line_count++; + s--; + if (s[0] != '\n' && s[0] != '\r') + line_count++; + *out_text_end = s; + return line_count; +} + +static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) +{ + ImFont* font = GImGui->Font; + const float line_height = GImGui->FontSize; + const float scale = line_height / font->FontSize; + + ImVec2 text_size = ImVec2(0,0); + float line_width = 0.0f; + + const ImWchar* s = text_begin; + while (s < text_end) + { + unsigned int c = (unsigned int)(*s++); + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + if (stop_on_new_line) + break; + continue; + } + if (c == '\r') + continue; + + const float char_width = font->GetCharAdvance((unsigned short)c) * scale; + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (out_offset) + *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n + + if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +// Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) +namespace ImGuiStb +{ + +static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING* obj) { return obj->CurLenW; } +static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING* obj, int idx) { return obj->Text[idx]; } +static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { ImWchar c = obj->Text[line_start_idx+char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; return GImGui->Font->GetCharAdvance(c) * (GImGui->FontSize / GImGui->Font->FontSize); } +static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x10000 ? 0 : key; } +static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; +static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx) +{ + const ImWchar* text = obj->Text.Data; + const ImWchar* text_remaining = NULL; + const ImVec2 size = InputTextCalcTextSizeW(text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); + r->x0 = 0.0f; + r->x1 = size.x; + r->baseline_y_delta = size.y; + r->ymin = 0.0f; + r->ymax = size.y; + r->num_chars = (int)(text_remaining - (text + line_start_idx)); +} + +static bool is_separator(unsigned int c) { return ImCharIsSpace(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|'; } +static int is_word_boundary_from_right(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (is_separator( obj->Text[idx-1] ) && !is_separator( obj->Text[idx] ) ) : 1; } +static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } +#ifdef __APPLE__ // FIXME: Move setting to IO structure +static int is_word_boundary_from_left(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (!is_separator( obj->Text[idx-1] ) && is_separator( obj->Text[idx] ) ) : 1; } +static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } +#else +static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } +#endif +#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h +#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL + +static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING* obj, int pos, int n) +{ + ImWchar* dst = obj->Text.Data + pos; + + // We maintain our buffer length in both UTF-8 and wchar formats + obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); + obj->CurLenW -= n; + + // Offset remaining text + const ImWchar* src = obj->Text.Data + pos + n; + while (ImWchar c = *src++) + *dst++ = c; + *dst = '\0'; +} + +static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const ImWchar* new_text, int new_text_len) +{ + const int text_len = obj->CurLenW; + IM_ASSERT(pos <= text_len); + if (new_text_len + text_len + 1 > obj->Text.Size) + return false; + + const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); + if (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufSizeA) + return false; + + ImWchar* text = obj->Text.Data; + if (pos != text_len) + memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar)); + memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar)); + + obj->CurLenW += new_text_len; + obj->CurLenA += new_text_len_utf8; + obj->Text[obj->CurLenW] = '\0'; + + return true; +} + +// We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols) +#define STB_TEXTEDIT_K_LEFT 0x10000 // keyboard input to move cursor left +#define STB_TEXTEDIT_K_RIGHT 0x10001 // keyboard input to move cursor right +#define STB_TEXTEDIT_K_UP 0x10002 // keyboard input to move cursor up +#define STB_TEXTEDIT_K_DOWN 0x10003 // keyboard input to move cursor down +#define STB_TEXTEDIT_K_LINESTART 0x10004 // keyboard input to move cursor to start of line +#define STB_TEXTEDIT_K_LINEEND 0x10005 // keyboard input to move cursor to end of line +#define STB_TEXTEDIT_K_TEXTSTART 0x10006 // keyboard input to move cursor to start of text +#define STB_TEXTEDIT_K_TEXTEND 0x10007 // keyboard input to move cursor to end of text +#define STB_TEXTEDIT_K_DELETE 0x10008 // keyboard input to delete selection or character under cursor +#define STB_TEXTEDIT_K_BACKSPACE 0x10009 // keyboard input to delete selection or character left of cursor +#define STB_TEXTEDIT_K_UNDO 0x1000A // keyboard input to perform undo +#define STB_TEXTEDIT_K_REDO 0x1000B // keyboard input to perform redo +#define STB_TEXTEDIT_K_WORDLEFT 0x1000C // keyboard input to move cursor left one word +#define STB_TEXTEDIT_K_WORDRIGHT 0x1000D // keyboard input to move cursor right one word +#define STB_TEXTEDIT_K_SHIFT 0x20000 + +#define STB_TEXTEDIT_IMPLEMENTATION +#include "stb_textedit.h" + +} + +void ImGuiTextEditState::OnKeyPressed(int key) +{ + stb_textedit_key(this, &StbState, key); + CursorFollow = true; + CursorAnimReset(); +} + +// Public API to manipulate UTF-8 text +// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar) +// FIXME: The existence of this rarely exercised code path is a bit of a nuisance. +void ImGuiTextEditCallbackData::DeleteChars(int pos, int bytes_count) +{ + IM_ASSERT(pos + bytes_count <= BufTextLen); + char* dst = Buf + pos; + const char* src = Buf + pos + bytes_count; + while (char c = *src++) + *dst++ = c; + *dst = '\0'; + + if (CursorPos + bytes_count >= pos) + CursorPos -= bytes_count; + else if (CursorPos >= pos) + CursorPos = pos; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen -= bytes_count; +} + +void ImGuiTextEditCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) +{ + const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text); + if (new_text_len + BufTextLen + 1 >= BufSize) + return; + + if (BufTextLen != pos) + memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos)); + memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char)); + Buf[BufTextLen + new_text_len] = '\0'; + + if (CursorPos >= pos) + CursorPos += new_text_len; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen += new_text_len; +} + +// Return false to discard a character. +static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data) +{ + unsigned int c = *p_char; + + if (c < 128 && c != ' ' && !isprint((int)(c & 0xFF))) + { + bool pass = false; + pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); + pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput)); + if (!pass) + return false; + } + + if (c >= 0xE000 && c <= 0xF8FF) // Filter private Unicode range. I don't imagine anybody would want to input them. GLFW on OSX seems to send private characters for special keys like arrow keys. + return false; + + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank)) + { + if (flags & ImGuiInputTextFlags_CharsDecimal) + if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/')) + return false; + + if (flags & ImGuiInputTextFlags_CharsHexadecimal) + if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) + return false; + + if (flags & ImGuiInputTextFlags_CharsUppercase) + if (c >= 'a' && c <= 'z') + *p_char = (c += (unsigned int)('A'-'a')); + + if (flags & ImGuiInputTextFlags_CharsNoBlank) + if (ImCharIsSpace(c)) + return false; + } + + if (flags & ImGuiInputTextFlags_CallbackCharFilter) + { + ImGuiTextEditCallbackData callback_data; + memset(&callback_data, 0, sizeof(ImGuiTextEditCallbackData)); + callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; + callback_data.EventChar = (ImWchar)c; + callback_data.Flags = flags; + callback_data.UserData = user_data; + if (callback(&callback_data) != 0) + return false; + *p_char = callback_data.EventChar; + if (!callback_data.EventChar) + return false; + } + + return true; +} + +// Edit a string of text +// NB: when active, hold on a privately held copy of the text (and apply back to 'buf'). So changing 'buf' while active has no effect. +// FIXME: Rather messy function partly because we are doing UTF8 > u16 > UTF8 conversions on the go to more easily handle stb_textedit calls. Ideally we should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188 +bool ImGui::InputTextEx(const char* label, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) + IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) + + ImGuiContext& g = *GImGui; + const ImGuiIO& io = g.IO; + const ImGuiStyle& style = g.Style; + + const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; + const bool is_editable = (flags & ImGuiInputTextFlags_ReadOnly) == 0; + const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; + const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; + + if (is_multiline) // Open group before calling GetID() because groups tracks id created during their spawn + BeginGroup(); + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? GetTextLineHeight() * 8.0f : label_size.y) + style.FramePadding.y*2.0f); // Arbitrary default of 8 lines high for multi-line + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? (style.ItemInnerSpacing.x + label_size.x) : 0.0f, 0.0f)); + + ImGuiWindow* draw_window = window; + if (is_multiline) + { + ItemAdd(total_bb, id, &frame_bb); + if (!BeginChildFrame(id, frame_bb.GetSize())) + { + EndChildFrame(); + EndGroup(); + return false; + } + draw_window = GetCurrentWindow(); + size.x -= draw_window->ScrollbarSizes.x; + } + else + { + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb)) + return false; + } + const bool hovered = ItemHoverable(frame_bb, id); + if (hovered) + g.MouseCursor = ImGuiMouseCursor_TextInput; + + // Password pushes a temporary font with only a fallback glyph + if (is_password) + { + const ImFontGlyph* glyph = g.Font->FindGlyph('*'); + ImFont* password_font = &g.InputTextPasswordFont; + password_font->FontSize = g.Font->FontSize; + password_font->Scale = g.Font->Scale; + password_font->DisplayOffset = g.Font->DisplayOffset; + password_font->Ascent = g.Font->Ascent; + password_font->Descent = g.Font->Descent; + password_font->ContainerAtlas = g.Font->ContainerAtlas; + password_font->FallbackGlyph = glyph; + password_font->FallbackAdvanceX = glyph->AdvanceX; + IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty()); + PushFont(password_font); + } + + // NB: we are only allowed to access 'edit_state' if we are the active widget. + ImGuiTextEditState& edit_state = g.InputTextState; + + const bool focus_requested = FocusableItemRegister(window, id, (flags & (ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_AllowTabInput)) == 0); // Using completion callback disable keyboard tabbing + const bool focus_requested_by_code = focus_requested && (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent); + const bool focus_requested_by_tab = focus_requested && !focus_requested_by_code; + + const bool user_clicked = hovered && io.MouseClicked[0]; + const bool user_scrolled = is_multiline && g.ActiveId == 0 && edit_state.Id == id && g.ActiveIdPreviousFrame == draw_window->GetIDNoKeepAlive("#SCROLLY"); + + bool clear_active_id = false; + + bool select_all = (g.ActiveId != id) && (((flags & ImGuiInputTextFlags_AutoSelectAll) != 0) || (g.NavInputId == id)) && (!is_multiline); + if (focus_requested || user_clicked || user_scrolled || g.NavInputId == id) + { + if (g.ActiveId != id) + { + // Start edition + // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar) + // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode) + const int prev_len_w = edit_state.CurLenW; + edit_state.Text.resize(buf_size+1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data isn't NULL so it doesn't crash. + edit_state.InitialText.resize(buf_size+1); // UTF-8. we use +1 to make sure that .Data isn't NULL so it doesn't crash. + ImStrncpy(edit_state.InitialText.Data, buf, edit_state.InitialText.Size); + const char* buf_end = NULL; + edit_state.CurLenW = ImTextStrFromUtf8(edit_state.Text.Data, edit_state.Text.Size, buf, NULL, &buf_end); + edit_state.CurLenA = (int)(buf_end - buf); // We can't get the result from ImFormatString() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. + edit_state.CursorAnimReset(); + + // Preserve cursor position and undo/redo stack if we come back to same widget + // FIXME: We should probably compare the whole buffer to be on the safety side. Comparing buf (utf8) and edit_state.Text (wchar). + const bool recycle_state = (edit_state.Id == id) && (prev_len_w == edit_state.CurLenW); + if (recycle_state) + { + // Recycle existing cursor/selection/undo stack but clamp position + // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. + edit_state.CursorClamp(); + } + else + { + edit_state.Id = id; + edit_state.ScrollX = 0.0f; + stb_textedit_initialize_state(&edit_state.StbState, !is_multiline); + if (!is_multiline && focus_requested_by_code) + select_all = true; + } + if (flags & ImGuiInputTextFlags_AlwaysInsertMode) + edit_state.StbState.insert_mode = true; + if (!is_multiline && (focus_requested_by_tab || (user_clicked && io.KeyCtrl))) + select_all = true; + } + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + if (!is_multiline && !(flags & ImGuiInputTextFlags_CallbackHistory)) + g.ActiveIdAllowNavDirFlags |= ((1 << ImGuiDir_Up) | (1 << ImGuiDir_Down)); + } + else if (io.MouseClicked[0]) + { + // Release focus when we click outside + clear_active_id = true; + } + + bool value_changed = false; + bool enter_pressed = false; + + if (g.ActiveId == id) + { + if (!is_editable && !g.ActiveIdIsJustActivated) + { + // When read-only we always use the live data passed to the function + edit_state.Text.resize(buf_size+1); + const char* buf_end = NULL; + edit_state.CurLenW = ImTextStrFromUtf8(edit_state.Text.Data, edit_state.Text.Size, buf, NULL, &buf_end); + edit_state.CurLenA = (int)(buf_end - buf); + edit_state.CursorClamp(); + } + + edit_state.BufSizeA = buf_size; + + // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. + // Down the line we should have a cleaner library-wide concept of Selected vs Active. + g.ActiveIdAllowOverlap = !io.MouseDown[0]; + g.WantTextInputNextFrame = 1; + + // Edit in progress + const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + edit_state.ScrollX; + const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y - style.FramePadding.y) : (g.FontSize*0.5f)); + + const bool osx_double_click_selects_words = io.OptMacOSXBehaviors; // OS X style: Double click selects by word instead of selecting whole text + if (select_all || (hovered && !osx_double_click_selects_words && io.MouseDoubleClicked[0])) + { + edit_state.SelectAll(); + edit_state.SelectedAllMouseLock = true; + } + else if (hovered && osx_double_click_selects_words && io.MouseDoubleClicked[0]) + { + // Select a word only, OS X style (by simulating keystrokes) + edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); + edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + } + else if (io.MouseClicked[0] && !edit_state.SelectedAllMouseLock) + { + if (hovered) + { + stb_textedit_click(&edit_state, &edit_state.StbState, mouse_x, mouse_y); + edit_state.CursorAnimReset(); + } + } + else if (io.MouseDown[0] && !edit_state.SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) + { + stb_textedit_drag(&edit_state, &edit_state.StbState, mouse_x, mouse_y); + edit_state.CursorAnimReset(); + edit_state.CursorFollow = true; + } + if (edit_state.SelectedAllMouseLock && !io.MouseDown[0]) + edit_state.SelectedAllMouseLock = false; + + if (io.InputCharacters[0]) + { + // Process text input (before we check for Return because using some IME will effectively send a Return?) + // We ignore CTRL inputs, but need to allow CTRL+ALT as some keyboards (e.g. German) use AltGR - which is Alt+Ctrl - to input certain characters. + if (!(io.KeyCtrl && !io.KeyAlt) && is_editable) + { + for (int n = 0; n < IM_ARRAYSIZE(io.InputCharacters) && io.InputCharacters[n]; n++) + if (unsigned int c = (unsigned int)io.InputCharacters[n]) + { + // Insert character if they pass filtering + if (!InputTextFilterCharacter(&c, flags, callback, user_data)) + continue; + edit_state.OnKeyPressed((int)c); + } + } + + // Consume characters + memset(g.IO.InputCharacters, 0, sizeof(g.IO.InputCharacters)); + } + } + + bool cancel_edit = false; + if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) + { + // Handle key-presses + const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); + const bool is_shortcut_key_only = (io.OptMacOSXBehaviors ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl + const bool is_wordmove_key_down = io.OptMacOSXBehaviors ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl + const bool is_startend_key_down = io.OptMacOSXBehaviors && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End + const bool is_ctrl_key_only = io.KeyCtrl && !io.KeyShift && !io.KeyAlt && !io.KeySuper; + const bool is_shift_key_only = io.KeyShift && !io.KeyCtrl && !io.KeyAlt && !io.KeySuper; + + const bool is_cut = ((is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Delete))) && is_editable && !is_password && (!is_multiline || edit_state.HasSelection()); + const bool is_copy = ((is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_password && (!is_multiline || edit_state.HasSelection()); + const bool is_paste = ((is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && is_editable; + + if (IsKeyPressedMap(ImGuiKey_LeftArrow)) { edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_RightArrow)) { edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetWindowScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetWindowScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_Home)) { edit_state.OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_End)) { edit_state.OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_Delete) && is_editable) { edit_state.OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); } + else if (IsKeyPressedMap(ImGuiKey_Backspace) && is_editable) + { + if (!edit_state.HasSelection()) + { + if (is_wordmove_key_down) edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT|STB_TEXTEDIT_K_SHIFT); + else if (io.OptMacOSXBehaviors && io.KeySuper && !io.KeyAlt && !io.KeyCtrl) edit_state.OnKeyPressed(STB_TEXTEDIT_K_LINESTART|STB_TEXTEDIT_K_SHIFT); + } + edit_state.OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); + } + else if (IsKeyPressedMap(ImGuiKey_Enter)) + { + bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; + if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) + { + enter_pressed = clear_active_id = true; + } + else if (is_editable) + { + unsigned int c = '\n'; // Insert new line + if (InputTextFilterCharacter(&c, flags, callback, user_data)) + edit_state.OnKeyPressed((int)c); + } + } + else if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressedMap(ImGuiKey_Tab) && !io.KeyCtrl && !io.KeyShift && !io.KeyAlt && is_editable) + { + unsigned int c = '\t'; // Insert TAB + if (InputTextFilterCharacter(&c, flags, callback, user_data)) + edit_state.OnKeyPressed((int)c); + } + else if (IsKeyPressedMap(ImGuiKey_Escape)) { clear_active_id = cancel_edit = true; } + else if (is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_Z) && is_editable && is_undoable) { edit_state.OnKeyPressed(STB_TEXTEDIT_K_UNDO); edit_state.ClearSelection(); } + else if (is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_Y) && is_editable && is_undoable) { edit_state.OnKeyPressed(STB_TEXTEDIT_K_REDO); edit_state.ClearSelection(); } + else if (is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_A)) { edit_state.SelectAll(); edit_state.CursorFollow = true; } + else if (is_cut || is_copy) + { + // Cut, Copy + if (io.SetClipboardTextFn) + { + const int ib = edit_state.HasSelection() ? ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end) : 0; + const int ie = edit_state.HasSelection() ? ImMax(edit_state.StbState.select_start, edit_state.StbState.select_end) : edit_state.CurLenW; + edit_state.TempTextBuffer.resize((ie-ib) * 4 + 1); + ImTextStrToUtf8(edit_state.TempTextBuffer.Data, edit_state.TempTextBuffer.Size, edit_state.Text.Data+ib, edit_state.Text.Data+ie); + SetClipboardText(edit_state.TempTextBuffer.Data); + } + + if (is_cut) + { + if (!edit_state.HasSelection()) + edit_state.SelectAll(); + edit_state.CursorFollow = true; + stb_textedit_cut(&edit_state, &edit_state.StbState); + } + } + else if (is_paste) + { + // Paste + if (const char* clipboard = GetClipboardText()) + { + // Filter pasted buffer + const int clipboard_len = (int)strlen(clipboard); + ImWchar* clipboard_filtered = (ImWchar*)ImGui::MemAlloc((clipboard_len+1) * sizeof(ImWchar)); + int clipboard_filtered_len = 0; + for (const char* s = clipboard; *s; ) + { + unsigned int c; + s += ImTextCharFromUtf8(&c, s, NULL); + if (c == 0) + break; + if (c >= 0x10000 || !InputTextFilterCharacter(&c, flags, callback, user_data)) + continue; + clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; + } + clipboard_filtered[clipboard_filtered_len] = 0; + if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation + { + stb_textedit_paste(&edit_state, &edit_state.StbState, clipboard_filtered, clipboard_filtered_len); + edit_state.CursorFollow = true; + } + ImGui::MemFree(clipboard_filtered); + } + } + } + + if (g.ActiveId == id) + { + if (cancel_edit) + { + // Restore initial value + if (is_editable) + { + ImStrncpy(buf, edit_state.InitialText.Data, buf_size); + value_changed = true; + } + } + + // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. + // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. Also this allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage. + bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); + if (apply_edit_back_to_user_buffer) + { + // Apply new value immediately - copy modified buffer back + // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer + // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect. + // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks. + if (is_editable) + { + edit_state.TempTextBuffer.resize(edit_state.Text.Size * 4); + ImTextStrToUtf8(edit_state.TempTextBuffer.Data, edit_state.TempTextBuffer.Size, edit_state.Text.Data, NULL); + } + + // User callback + if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways)) != 0) + { + IM_ASSERT(callback != NULL); + + // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. + ImGuiInputTextFlags event_flag = 0; + ImGuiKey event_key = ImGuiKey_COUNT; + if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressedMap(ImGuiKey_Tab)) + { + event_flag = ImGuiInputTextFlags_CallbackCompletion; + event_key = ImGuiKey_Tab; + } + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_UpArrow)) + { + event_flag = ImGuiInputTextFlags_CallbackHistory; + event_key = ImGuiKey_UpArrow; + } + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_DownArrow)) + { + event_flag = ImGuiInputTextFlags_CallbackHistory; + event_key = ImGuiKey_DownArrow; + } + else if (flags & ImGuiInputTextFlags_CallbackAlways) + event_flag = ImGuiInputTextFlags_CallbackAlways; + + if (event_flag) + { + ImGuiTextEditCallbackData callback_data; + memset(&callback_data, 0, sizeof(ImGuiTextEditCallbackData)); + callback_data.EventFlag = event_flag; + callback_data.Flags = flags; + callback_data.UserData = user_data; + callback_data.ReadOnly = !is_editable; + + callback_data.EventKey = event_key; + callback_data.Buf = edit_state.TempTextBuffer.Data; + callback_data.BufTextLen = edit_state.CurLenA; + callback_data.BufSize = edit_state.BufSizeA; + callback_data.BufDirty = false; + + // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) + ImWchar* text = edit_state.Text.Data; + const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.cursor); + const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.select_start); + const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.select_end); + + // Call user code + callback(&callback_data); + + // Read back what user may have modified + IM_ASSERT(callback_data.Buf == edit_state.TempTextBuffer.Data); // Invalid to modify those fields + IM_ASSERT(callback_data.BufSize == edit_state.BufSizeA); + IM_ASSERT(callback_data.Flags == flags); + if (callback_data.CursorPos != utf8_cursor_pos) edit_state.StbState.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); + if (callback_data.SelectionStart != utf8_selection_start) edit_state.StbState.select_start = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); + if (callback_data.SelectionEnd != utf8_selection_end) edit_state.StbState.select_end = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); + if (callback_data.BufDirty) + { + IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! + edit_state.CurLenW = ImTextStrFromUtf8(edit_state.Text.Data, edit_state.Text.Size, callback_data.Buf, NULL); + edit_state.CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() + edit_state.CursorAnimReset(); + } + } + } + + // Copy back to user buffer + if (is_editable && strcmp(edit_state.TempTextBuffer.Data, buf) != 0) + { + ImStrncpy(buf, edit_state.TempTextBuffer.Data, buf_size); + value_changed = true; + } + } + } + + // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) + if (clear_active_id && g.ActiveId == id) + ClearActiveID(); + + // Render + // Select which buffer we are going to display. When ImGuiInputTextFlags_NoLiveEdit is set 'buf' might still be the old value. We set buf to NULL to prevent accidental usage from now on. + const char* buf_display = (g.ActiveId == id && is_editable) ? edit_state.TempTextBuffer.Data : buf; buf = NULL; + + RenderNavHighlight(frame_bb, id); + if (!is_multiline) + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + size.x, frame_bb.Min.y + size.y); // Not using frame_bb.Max because we have adjusted size + ImVec2 render_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; + ImVec2 text_size(0.f, 0.f); + const bool is_currently_scrolling = (edit_state.Id == id && is_multiline && g.ActiveId == draw_window->GetIDNoKeepAlive("#SCROLLY")); + if (g.ActiveId == id || is_currently_scrolling) + { + edit_state.CursorAnim += io.DeltaTime; + + // This is going to be messy. We need to: + // - Display the text (this alone can be more easily clipped) + // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation) + // - Measure text height (for scrollbar) + // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) + // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. + const ImWchar* text_begin = edit_state.Text.Data; + ImVec2 cursor_offset, select_start_offset; + + { + // Count lines + find lines numbers straddling 'cursor' and 'select_start' position. + const ImWchar* searches_input_ptr[2]; + searches_input_ptr[0] = text_begin + edit_state.StbState.cursor; + searches_input_ptr[1] = NULL; + int searches_remaining = 1; + int searches_result_line_number[2] = { -1, -999 }; + if (edit_state.StbState.select_start != edit_state.StbState.select_end) + { + searches_input_ptr[1] = text_begin + ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end); + searches_result_line_number[1] = -1; + searches_remaining++; + } + + // Iterate all lines to find our line numbers + // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. + searches_remaining += is_multiline ? 1 : 0; + int line_count = 0; + for (const ImWchar* s = text_begin; *s != 0; s++) + if (*s == '\n') + { + line_count++; + if (searches_result_line_number[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_number[0] = line_count; if (--searches_remaining <= 0) break; } + if (searches_result_line_number[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_number[1] = line_count; if (--searches_remaining <= 0) break; } + } + line_count++; + if (searches_result_line_number[0] == -1) searches_result_line_number[0] = line_count; + if (searches_result_line_number[1] == -1) searches_result_line_number[1] = line_count; + + // Calculate 2d position by finding the beginning of the line and measuring distance + cursor_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; + cursor_offset.y = searches_result_line_number[0] * g.FontSize; + if (searches_result_line_number[1] >= 0) + { + select_start_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; + select_start_offset.y = searches_result_line_number[1] * g.FontSize; + } + + // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) + if (is_multiline) + text_size = ImVec2(size.x, line_count * g.FontSize); + } + + // Scroll + if (edit_state.CursorFollow) + { + // Horizontal scroll in chunks of quarter width + if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) + { + const float scroll_increment_x = size.x * 0.25f; + if (cursor_offset.x < edit_state.ScrollX) + edit_state.ScrollX = (float)(int)ImMax(0.0f, cursor_offset.x - scroll_increment_x); + else if (cursor_offset.x - size.x >= edit_state.ScrollX) + edit_state.ScrollX = (float)(int)(cursor_offset.x - size.x + scroll_increment_x); + } + else + { + edit_state.ScrollX = 0.0f; + } + + // Vertical scroll + if (is_multiline) + { + float scroll_y = draw_window->Scroll.y; + if (cursor_offset.y - g.FontSize < scroll_y) + scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); + else if (cursor_offset.y - size.y >= scroll_y) + scroll_y = cursor_offset.y - size.y; + draw_window->DC.CursorPos.y += (draw_window->Scroll.y - scroll_y); // To avoid a frame of lag + draw_window->Scroll.y = scroll_y; + render_pos.y = draw_window->DC.CursorPos.y; + } + } + edit_state.CursorFollow = false; + const ImVec2 render_scroll = ImVec2(edit_state.ScrollX, 0.0f); + + // Draw selection + if (edit_state.StbState.select_start != edit_state.StbState.select_end) + { + const ImWchar* text_selected_begin = text_begin + ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end); + const ImWchar* text_selected_end = text_begin + ImMax(edit_state.StbState.select_start, edit_state.StbState.select_end); + + float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. + float bg_offy_dn = is_multiline ? 0.0f : 2.0f; + ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg); + ImVec2 rect_pos = render_pos + select_start_offset - render_scroll; + for (const ImWchar* p = text_selected_begin; p < text_selected_end; ) + { + if (rect_pos.y > clip_rect.w + g.FontSize) + break; + if (rect_pos.y < clip_rect.y) + { + while (p < text_selected_end) + if (*p++ == '\n') + break; + } + else + { + ImVec2 rect_size = InputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true); + if (rect_size.x <= 0.0f) rect_size.x = (float)(int)(g.Font->GetCharAdvance((unsigned short)' ') * 0.50f); // So we can see selected empty lines + ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos +ImVec2(rect_size.x, bg_offy_dn)); + rect.ClipWith(clip_rect); + if (rect.Overlaps(clip_rect)) + draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); + } + rect_pos.x = render_pos.x - render_scroll.x; + rect_pos.y += g.FontSize; + } + } + + draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos - render_scroll, GetColorU32(ImGuiCol_Text), buf_display, buf_display + edit_state.CurLenA, 0.0f, is_multiline ? NULL : &clip_rect); + + // Draw blinking cursor + bool cursor_is_visible = (!g.IO.OptCursorBlink) || (g.InputTextState.CursorAnim <= 0.0f) || fmodf(g.InputTextState.CursorAnim, 1.20f) <= 0.80f; + ImVec2 cursor_screen_pos = render_pos + cursor_offset - render_scroll; + ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y-g.FontSize+0.5f, cursor_screen_pos.x+1.0f, cursor_screen_pos.y-1.5f); + if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) + draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); + + // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) + if (is_editable) + g.OsImePosRequest = ImVec2(cursor_screen_pos.x - 1, cursor_screen_pos.y - g.FontSize); + } + else + { + // Render text only + const char* buf_end = NULL; + if (is_multiline) + text_size = ImVec2(size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_end) * g.FontSize); // We don't need width + draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos, GetColorU32(ImGuiCol_Text), buf_display, buf_end, 0.0f, is_multiline ? NULL : &clip_rect); + } + + if (is_multiline) + { + Dummy(text_size + ImVec2(0.0f, g.FontSize)); // Always add room to scroll an extra line + EndChildFrame(); + EndGroup(); + } + + if (is_password) + PopFont(); + + // Log as text + if (g.LogEnabled && !is_password) + LogRenderedText(&render_pos, buf_display, NULL); + + if (label_size.x > 0) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) + return enter_pressed; + else + return value_changed; +} + +bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data) +{ + IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() + return InputTextEx(label, buf, (int)buf_size, ImVec2(0,0), flags, callback, user_data); +} + +bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void* user_data) +{ + return InputTextEx(label, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data); +} + +// NB: scalar_format here must be a simple "%xx" format string with no prefix/suffix (unlike the Drag/Slider functions "display_format" argument) +bool ImGui::InputScalarEx(const char* label, ImGuiDataType data_type, void* data_ptr, void* step_ptr, void* step_fast_ptr, const char* scalar_format, ImGuiInputTextFlags extra_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + BeginGroup(); + PushID(label); + const ImVec2 button_sz = ImVec2(GetFrameHeight(), GetFrameHeight()); + if (step_ptr) + PushItemWidth(ImMax(1.0f, CalcItemWidth() - (button_sz.x + style.ItemInnerSpacing.x)*2)); + + char buf[64]; + DataTypeFormatString(data_type, data_ptr, scalar_format, buf, IM_ARRAYSIZE(buf)); + + bool value_changed = false; + if (!(extra_flags & ImGuiInputTextFlags_CharsHexadecimal)) + extra_flags |= ImGuiInputTextFlags_CharsDecimal; + extra_flags |= ImGuiInputTextFlags_AutoSelectAll; + if (InputText("", buf, IM_ARRAYSIZE(buf), extra_flags)) // PushId(label) + "" gives us the expected ID from outside point of view + value_changed = DataTypeApplyOpFromText(buf, GImGui->InputTextState.InitialText.begin(), data_type, data_ptr, scalar_format); + + // Step buttons + if (step_ptr) + { + PopItemWidth(); + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("-", button_sz, ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups)) + { + DataTypeApplyOp(data_type, '-', data_ptr, g.IO.KeyCtrl && step_fast_ptr ? step_fast_ptr : step_ptr); + value_changed = true; + } + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("+", button_sz, ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups)) + { + DataTypeApplyOp(data_type, '+', data_ptr, g.IO.KeyCtrl && step_fast_ptr ? step_fast_ptr : step_ptr); + value_changed = true; + } + } + PopID(); + + if (label_size.x > 0) + { + SameLine(0, style.ItemInnerSpacing.x); + RenderText(ImVec2(window->DC.CursorPos.x, window->DC.CursorPos.y + style.FramePadding.y), label); + ItemSize(label_size, style.FramePadding.y); + } + EndGroup(); + + return value_changed; +} + +bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags extra_flags) +{ + char display_format[16]; + if (decimal_precision < 0) + strcpy(display_format, "%f"); // Ideally we'd have a minimum decimal precision of 1 to visually denote that this is a float, while hiding non-significant digits? %f doesn't have a minimum of 1 + else + ImFormatString(display_format, IM_ARRAYSIZE(display_format), "%%.%df", decimal_precision); + return InputScalarEx(label, ImGuiDataType_Float, (void*)v, (void*)(step>0.0f ? &step : NULL), (void*)(step_fast>0.0f ? &step_fast : NULL), display_format, extra_flags); +} + +bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags extra_flags) +{ + // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes. + const char* scalar_format = (extra_flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d"; + return InputScalarEx(label, ImGuiDataType_Int, (void*)v, (void*)(step>0.0f ? &step : NULL), (void*)(step_fast>0.0f ? &step_fast : NULL), scalar_format, extra_flags); +} + +bool ImGui::InputFloatN(const char* label, float* v, int components, int decimal_precision, ImGuiInputTextFlags extra_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components); + for (int i = 0; i < components; i++) + { + PushID(i); + value_changed |= InputFloat("##v", &v[i], 0, 0, decimal_precision, extra_flags); + SameLine(0, g.Style.ItemInnerSpacing.x); + PopID(); + PopItemWidth(); + } + PopID(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::InputFloat2(const char* label, float v[2], int decimal_precision, ImGuiInputTextFlags extra_flags) +{ + return InputFloatN(label, v, 2, decimal_precision, extra_flags); +} + +bool ImGui::InputFloat3(const char* label, float v[3], int decimal_precision, ImGuiInputTextFlags extra_flags) +{ + return InputFloatN(label, v, 3, decimal_precision, extra_flags); +} + +bool ImGui::InputFloat4(const char* label, float v[4], int decimal_precision, ImGuiInputTextFlags extra_flags) +{ + return InputFloatN(label, v, 4, decimal_precision, extra_flags); +} + +bool ImGui::InputIntN(const char* label, int* v, int components, ImGuiInputTextFlags extra_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components); + for (int i = 0; i < components; i++) + { + PushID(i); + value_changed |= InputInt("##v", &v[i], 0, 0, extra_flags); + SameLine(0, g.Style.ItemInnerSpacing.x); + PopID(); + PopItemWidth(); + } + PopID(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags extra_flags) +{ + return InputIntN(label, v, 2, extra_flags); +} + +bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags extra_flags) +{ + return InputIntN(label, v, 3, extra_flags); +} + +bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags extra_flags) +{ + return InputIntN(label, v, 4, extra_flags); +} + +static float CalcMaxPopupHeightFromItemCount(int items_count) +{ + ImGuiContext& g = *GImGui; + if (items_count <= 0) + return FLT_MAX; + return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2); +} + +bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) +{ + // Always consume the SetNextWindowSizeConstraint() call in our early return paths + ImGuiContext& g = *GImGui; + ImGuiCond backup_next_window_size_constraint = g.NextWindowData.SizeConstraintCond; + g.NextWindowData.SizeConstraintCond = 0; + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held); + bool popup_open = IsPopupOpen(id); + + const float arrow_size = GetFrameHeight(); + const ImRect value_bb(frame_bb.Min, frame_bb.Max - ImVec2(arrow_size, 0.0f)); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + RenderFrame(ImVec2(frame_bb.Max.x-arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32(popup_open || hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button), true, style.FrameRounding); // FIXME-ROUNDING + RenderTriangle(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); + if (preview_value != NULL) + RenderTextClipped(frame_bb.Min + style.FramePadding, value_bb.Max, preview_value, NULL, NULL, ImVec2(0.0f,0.0f)); + if (label_size.x > 0) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + if ((pressed || g.NavActivateId == id) && !popup_open) + { + if (window->DC.NavLayerCurrent == 0) + window->NavLastIds[0] = id; + OpenPopupEx(id); + popup_open = true; + } + + if (!popup_open) + return false; + + if (backup_next_window_size_constraint) + { + g.NextWindowData.SizeConstraintCond = backup_next_window_size_constraint; + g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); + } + else + { + if ((flags & ImGuiComboFlags_HeightMask_) == 0) + flags |= ImGuiComboFlags_HeightRegular; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one + int popup_max_height_in_items = -1; + if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8; + else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4; + else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20; + SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); + } + + char name[16]; + ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth + + // Peak into expected window size so we can position it + if (ImGuiWindow* popup_window = FindWindowByName(name)) + if (popup_window->WasActive) + { + ImVec2 size_contents = CalcSizeContents(popup_window); + ImVec2 size_expected = CalcSizeAfterConstraint(popup_window, CalcSizeAutoFit(popup_window, size_contents)); + if (flags & ImGuiComboFlags_PopupAlignLeft) + popup_window->AutoPosLastDirection = ImGuiDir_Left; + ImVec2 pos = FindBestWindowPosForPopup(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, frame_bb, ImGuiPopupPositionPolicy_ComboBox); + SetNextWindowPos(pos); + } + + ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + if (!Begin(name, NULL, window_flags)) + { + EndPopup(); + IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above + return false; + } + + // Horizontally align ourselves with the framed text + if (style.FramePadding.x != style.WindowPadding.x) + Indent(style.FramePadding.x - style.WindowPadding.x); + + return true; +} + +void ImGui::EndCombo() +{ + const ImGuiStyle& style = GImGui->Style; + if (style.FramePadding.x != style.WindowPadding.x) + Unindent(style.FramePadding.x - style.WindowPadding.x); + EndPopup(); +} + +// Old API, prefer using BeginCombo() nowadays if you can. +bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items) +{ + ImGuiContext& g = *GImGui; + + const char* preview_text = NULL; + if (*current_item >= 0 && *current_item < items_count) + items_getter(data, *current_item, &preview_text); + + // The old Combo() API exposed "popup_max_height_in_items", however the new more general BeginCombo() API doesn't, so we emulate it here. + if (popup_max_height_in_items != -1 && !g.NextWindowData.SizeConstraintCond) + { + float popup_max_height = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); + SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, popup_max_height)); + } + + if (!BeginCombo(label, preview_text, 0)) + return false; + + // Display items + // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed) + bool value_changed = false; + for (int i = 0; i < items_count; i++) + { + PushID((void*)(intptr_t)i); + const bool item_selected = (i == *current_item); + const char* item_text; + if (!items_getter(data, i, &item_text)) + item_text = "*Unknown item*"; + if (Selectable(item_text, item_selected)) + { + value_changed = true; + *current_item = i; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); + } + + EndCombo(); + return value_changed; +} + +static bool Items_ArrayGetter(void* data, int idx, const char** out_text) +{ + const char* const* items = (const char* const*)data; + if (out_text) + *out_text = items[idx]; + return true; +} + +static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) +{ + // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. + const char* items_separated_by_zeros = (const char*)data; + int items_count = 0; + const char* p = items_separated_by_zeros; + while (*p) + { + if (idx == items_count) + break; + p += strlen(p) + 1; + items_count++; + } + if (!*p) + return false; + if (out_text) + *out_text = p; + return true; +} + +// Combo box helper allowing to pass an array of strings. +bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) +{ + const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items); + return value_changed; +} + +// Combo box helper allowing to pass all items in a single string. +bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items) +{ + int items_count = 0; + const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open + while (*p) + { + p += strlen(p) + 1; + items_count++; + } + bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items); + return value_changed; +} + +// Tip: pass an empty label (e.g. "##dummy") then you can use the space to draw other text or image. +// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID. +bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) // FIXME-OPT: Avoid if vertically clipped. + PopClipRect(); + + ImGuiID id = window->GetID(label); + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrentLineTextBaseOffset; + ImRect bb(pos, pos + size); + ItemSize(bb); + + // Fill horizontal space. + ImVec2 window_padding = window->WindowPadding; + float max_x = (flags & ImGuiSelectableFlags_SpanAllColumns) ? GetWindowContentRegionMax().x : GetContentRegionMax().x; + float w_draw = ImMax(label_size.x, window->Pos.x + max_x - window_padding.x - window->DC.CursorPos.x); + ImVec2 size_draw((size_arg.x != 0 && !(flags & ImGuiSelectableFlags_DrawFillAvailWidth)) ? size_arg.x : w_draw, size_arg.y != 0.0f ? size_arg.y : size.y); + ImRect bb_with_spacing(pos, pos + size_draw); + if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth)) + bb_with_spacing.Max.x += window_padding.x; + + // Selectables are tightly packed together, we extend the box to cover spacing between selectable. + float spacing_L = (float)(int)(style.ItemSpacing.x * 0.5f); + float spacing_U = (float)(int)(style.ItemSpacing.y * 0.5f); + float spacing_R = style.ItemSpacing.x - spacing_L; + float spacing_D = style.ItemSpacing.y - spacing_U; + bb_with_spacing.Min.x -= spacing_L; + bb_with_spacing.Min.y -= spacing_U; + bb_with_spacing.Max.x += spacing_R; + bb_with_spacing.Max.y += spacing_D; + if (!ItemAdd(bb_with_spacing, (flags & ImGuiSelectableFlags_Disabled) ? 0 : id)) + { + if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) + PushColumnClipRect(); + return false; + } + + ImGuiButtonFlags button_flags = 0; + if (flags & ImGuiSelectableFlags_Menu) button_flags |= ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_NoHoldingActiveID; + if (flags & ImGuiSelectableFlags_MenuItem) button_flags |= ImGuiButtonFlags_PressedOnRelease; + if (flags & ImGuiSelectableFlags_Disabled) button_flags |= ImGuiButtonFlags_Disabled; + if (flags & ImGuiSelectableFlags_AllowDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; + bool hovered, held; + bool pressed = ButtonBehavior(bb_with_spacing, id, &hovered, &held, button_flags); + if (flags & ImGuiSelectableFlags_Disabled) + selected = false; + + // Hovering selectable with mouse updates NavId accordingly so navigation can be resumed with gamepad/keyboard (this doesn't happen on most widgets) + if (pressed || hovered)// && (g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f)) + if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerActiveMask) + { + g.NavDisableHighlight = true; + SetNavID(id, window->DC.NavLayerCurrent); + } + + // Render + if (hovered || selected) + { + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb_with_spacing.Min, bb_with_spacing.Max, col, false, 0.0f); + RenderNavHighlight(bb_with_spacing, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + } + + if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) + { + PushColumnClipRect(); + bb_with_spacing.Max.x -= (GetContentRegionMax().x - max_x); + } + + if (flags & ImGuiSelectableFlags_Disabled) PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); + RenderTextClipped(bb.Min, bb_with_spacing.Max, label, NULL, &label_size, ImVec2(0.0f,0.0f)); + if (flags & ImGuiSelectableFlags_Disabled) PopStyleColor(); + + // Automatically close popups + if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup)) + CloseCurrentPopup(); + return pressed; +} + +bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) +{ + if (Selectable(label, *p_selected, flags, size_arg)) + { + *p_selected = !*p_selected; + return true; + } + return false; +} + +// Helper to calculate the size of a listbox and display a label on the right. +// Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an empty label "##empty" +bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiStyle& style = GetStyle(); + const ImGuiID id = GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. + ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y); + ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); + ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy. + + BeginGroup(); + if (label_size.x > 0) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + BeginChildFrame(id, frame_bb.GetSize()); + return true; +} + +bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items) +{ + // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. + // However we don't add +0.40f if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size. + // I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution. + if (height_in_items < 0) + height_in_items = ImMin(items_count, 7); + float height_in_items_f = height_in_items < items_count ? (height_in_items + 0.40f) : (height_in_items + 0.00f); + + // We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild(). + ImVec2 size; + size.x = 0.0f; + size.y = GetTextLineHeightWithSpacing() * height_in_items_f + GetStyle().ItemSpacing.y; + return ListBoxHeader(label, size); +} + +void ImGui::ListBoxFooter() +{ + ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow; + const ImRect bb = parent_window->DC.LastItemRect; + const ImGuiStyle& style = GetStyle(); + + EndChildFrame(); + + // Redeclare item size so that it includes the label (we have stored the full size in LastItemRect) + // We call SameLine() to restore DC.CurrentLine* data + SameLine(); + parent_window->DC.CursorPos = bb.Min; + ItemSize(bb, style.FramePadding.y); + EndGroup(); +} + +bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items) +{ + const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items); + return value_changed; +} + +bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) +{ + if (!ListBoxHeader(label, items_count, height_in_items)) + return false; + + // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper. + bool value_changed = false; + ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + const bool item_selected = (i == *current_item); + const char* item_text; + if (!items_getter(data, i, &item_text)) + item_text = "*Unknown item*"; + + PushID(i); + if (Selectable(item_text, item_selected)) + { + *current_item = i; + value_changed = true; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); + } + ListBoxFooter(); + return value_changed; +} + +bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImVec2 pos = window->DC.CursorPos; + ImVec2 label_size = CalcTextSize(label, NULL, true); + + ImGuiSelectableFlags flags = ImGuiSelectableFlags_MenuItem | (enabled ? 0 : ImGuiSelectableFlags_Disabled); + bool pressed; + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + { + // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful + // Note that in this situation we render neither the shortcut neither the selected tick mark + float w = label_size.x; + window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f); + PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f); + pressed = Selectable(label, false, flags, ImVec2(w, 0.0f)); + PopStyleVar(); + window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + } + else + { + ImVec2 shortcut_size = shortcut ? CalcTextSize(shortcut, NULL) : ImVec2(0.0f, 0.0f); + float w = window->MenuColumns.DeclColumns(label_size.x, shortcut_size.x, (float)(int)(g.FontSize * 1.20f)); // Feedback for next frame + float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); + pressed = Selectable(label, false, flags | ImGuiSelectableFlags_DrawFillAvailWidth, ImVec2(w, 0.0f)); + if (shortcut_size.x > 0.0f) + { + PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); + RenderText(pos + ImVec2(window->MenuColumns.Pos[1] + extra_w, 0.0f), shortcut, NULL, false); + PopStyleColor(); + } + if (selected) + RenderCheckMark(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled), g.FontSize * 0.866f); + } + return pressed; +} + +bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) +{ + if (MenuItem(label, shortcut, p_selected ? *p_selected : false, enabled)) + { + if (p_selected) + *p_selected = !*p_selected; + return true; + } + return false; +} + +bool ImGui::BeginMainMenuBar() +{ + ImGuiContext& g = *GImGui; + SetNextWindowPos(ImVec2(0.0f, 0.0f)); + SetNextWindowSize(ImVec2(g.IO.DisplaySize.x, g.FontBaseSize + g.Style.FramePadding.y * 2.0f)); + PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0,0)); + if (!Begin("##MainMenuBar", NULL, ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_MenuBar) + || !BeginMenuBar()) + { + End(); + PopStyleVar(2); + return false; + } + g.CurrentWindow->DC.MenuBarOffsetX += g.Style.DisplaySafeAreaPadding.x; + return true; +} + +void ImGui::EndMainMenuBar() +{ + EndMenuBar(); + + // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window + ImGuiContext& g = *GImGui; + if (g.CurrentWindow == g.NavWindow && g.NavLayer == 0) + FocusFrontMostActiveWindow(g.NavWindow); + + End(); + PopStyleVar(2); +} + +bool ImGui::BeginMenuBar() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + if (!(window->Flags & ImGuiWindowFlags_MenuBar)) + return false; + + IM_ASSERT(!window->DC.MenuBarAppending); + BeginGroup(); // Save position + PushID("##menubar"); + + // We don't clip with regular window clipping rectangle as it is already set to the area below. However we clip with window full rect. + // We remove 1 worth of rounding to Max.x to that text in long menus don't tend to display over the lower-right rounded area, which looks particularly glitchy. + ImRect bar_rect = window->MenuBarRect(); + ImRect clip_rect(ImFloor(bar_rect.Min.x + 0.5f), ImFloor(bar_rect.Min.y + window->WindowBorderSize + 0.5f), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - window->WindowRounding) + 0.5f), ImFloor(bar_rect.Max.y + 0.5f)); + clip_rect.ClipWith(window->WindowRectClipped); + PushClipRect(clip_rect.Min, clip_rect.Max, false); + + window->DC.CursorPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffsetX, bar_rect.Min.y);// + g.Style.FramePadding.y); + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.NavLayerCurrent++; + window->DC.NavLayerCurrentMask <<= 1; + window->DC.MenuBarAppending = true; + AlignTextToFramePadding(); + return true; +} + +void ImGui::EndMenuBar() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. + if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + { + ImGuiWindow* nav_earliest_child = g.NavWindow; + while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) + nav_earliest_child = nav_earliest_child->ParentWindow; + if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && g.NavMoveRequestForward == ImGuiNavForward_None) + { + // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. + // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth the hassle/cost) + IM_ASSERT(window->DC.NavLayerActiveMaskNext & 0x02); // Sanity check + FocusWindow(window); + SetNavIDAndMoveMouse(window->NavLastIds[1], 1, window->NavRectRel[1]); + g.NavLayer = 1; + g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection. + g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; + NavMoveRequestCancel(); + } + } + + IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); + IM_ASSERT(window->DC.MenuBarAppending); + PopClipRect(); + PopID(); + window->DC.MenuBarOffsetX = window->DC.CursorPos.x - window->MenuBarRect().Min.x; + window->DC.GroupStack.back().AdvanceCursor = false; + EndGroup(); + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.NavLayerCurrent--; + window->DC.NavLayerCurrentMask >>= 1; + window->DC.MenuBarAppending = false; +} + +bool ImGui::BeginMenu(const char* label, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + ImVec2 label_size = CalcTextSize(label, NULL, true); + + bool pressed; + bool menu_is_open = IsPopupOpen(id); + bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].OpenParentId == window->IDStack.back()); + ImGuiWindow* backed_nav_window = g.NavWindow; + if (menuset_is_open) + g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent) + + // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu (using FindBestPopupWindowPos). + ImVec2 popup_pos, pos = window->DC.CursorPos; + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + { + // Menu inside an horizontal menu bar + // Selectable extend their highlight by half ItemSpacing in each direction. + // For ChildMenu, the popup position will be overwritten by the call to FindBestPopupWindowPos() in Begin() + popup_pos = ImVec2(pos.x - window->WindowPadding.x, pos.y - style.FramePadding.y + window->MenuBarHeight()); + window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f); + PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f); + float w = label_size.x; + pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_Menu | ImGuiSelectableFlags_DontClosePopups | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); + PopStyleVar(); + window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + } + else + { + // Menu inside a menu + popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); + float w = window->MenuColumns.DeclColumns(label_size.x, 0.0f, (float)(int)(g.FontSize * 1.20f)); // Feedback to next frame + float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); + pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_Menu | ImGuiSelectableFlags_DontClosePopups | ImGuiSelectableFlags_DrawFillAvailWidth | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); + if (!enabled) PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); + RenderTriangle(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.30f, 0.0f), ImGuiDir_Right); + if (!enabled) PopStyleColor(); + } + + const bool hovered = enabled && ItemHoverable(window->DC.LastItemRect, id); + if (menuset_is_open) + g.NavWindow = backed_nav_window; + + bool want_open = false, want_close = false; + if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) + { + // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. + bool moving_within_opened_triangle = false; + if (g.HoveredWindow == window && g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentWindow == window && !(window->Flags & ImGuiWindowFlags_MenuBar)) + { + if (ImGuiWindow* next_window = g.OpenPopupStack[g.CurrentPopupStack.Size].Window) + { + ImRect next_window_rect = next_window->Rect(); + ImVec2 ta = g.IO.MousePos - g.IO.MouseDelta; + ImVec2 tb = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR(); + ImVec2 tc = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetBL() : next_window_rect.GetBR(); + float extra = ImClamp(fabsf(ta.x - tb.x) * 0.30f, 5.0f, 30.0f); // add a bit of extra slack. + ta.x += (window->Pos.x < next_window->Pos.x) ? -0.5f : +0.5f; // to avoid numerical issues + tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -100.0f); // triangle is maximum 200 high to limit the slope and the bias toward large sub-menus // FIXME: Multiply by fb_scale? + tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +100.0f); + moving_within_opened_triangle = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); + //window->DrawList->PushClipRectFullScreen(); window->DrawList->AddTriangleFilled(ta, tb, tc, moving_within_opened_triangle ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); window->DrawList->PopClipRect(); // Debug + } + } + + want_close = (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_within_opened_triangle); + want_open = (!menu_is_open && hovered && !moving_within_opened_triangle) || (!menu_is_open && hovered && pressed); + + if (g.NavActivateId == id) + { + want_close = menu_is_open; + want_open = !menu_is_open; + } + if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open + { + want_open = true; + NavMoveRequestCancel(); + } + } + else + { + // Menu bar + if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it + { + want_close = true; + want_open = menu_is_open = false; + } + else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others + { + want_open = true; + } + else if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open + { + want_open = true; + NavMoveRequestCancel(); + } + } + + if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }' + want_close = true; + if (want_close && IsPopupOpen(id)) + ClosePopupToLevel(g.CurrentPopupStack.Size); + + if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.CurrentPopupStack.Size) + { + // Don't recycle same menu level in the same frame, first close the other menu and yield for a frame. + OpenPopup(label); + return false; + } + + menu_is_open |= want_open; + if (want_open) + OpenPopup(label); + + if (menu_is_open) + { + SetNextWindowPos(popup_pos, ImGuiCond_Always); + ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ((window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) ? ImGuiWindowFlags_ChildMenu|ImGuiWindowFlags_ChildWindow : ImGuiWindowFlags_ChildMenu); + menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + } + + return menu_is_open; +} + +void ImGui::EndMenu() +{ + // Nav: When a left move request _within our child menu_ failed, close the menu. + // A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs. + // However it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavWindow && g.NavWindow->ParentWindow == window && g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical) + { + ClosePopupToLevel(g.OpenPopupStack.Size - 1); + NavMoveRequestCancel(); + } + + EndPopup(); +} + +// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + BeginTooltipEx(0, true); + + const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; + if (text_end > text) + { + TextUnformatted(text, text_end); + Separator(); + } + + ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); + ColorButton("##preview", ImVec4(col[0], col[1], col[2], col[3]), (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); + SameLine(); + if (flags & ImGuiColorEditFlags_NoAlpha) + Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); + else + Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]); + EndTooltip(); +} + +static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) +{ + float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; + int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); + return IM_COL32(r, g, b, 0xFF); +} + +// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. +// I spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding alltogether. +void ImGui::RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, int rounding_corners_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) + { + ImU32 col_bg1 = GetColorU32(ImAlphaBlendColor(IM_COL32(204,204,204,255), col)); + ImU32 col_bg2 = GetColorU32(ImAlphaBlendColor(IM_COL32(128,128,128,255), col)); + window->DrawList->AddRectFilled(p_min, p_max, col_bg1, rounding, rounding_corners_flags); + + int yi = 0; + for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) + { + float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); + if (y2 <= y1) + continue; + for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) + { + float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); + if (x2 <= x1) + continue; + int rounding_corners_flags_cell = 0; + if (y1 <= p_min.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopRight; } + if (y2 >= p_max.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotRight; } + rounding_corners_flags_cell &= rounding_corners_flags; + window->DrawList->AddRectFilled(ImVec2(x1,y1), ImVec2(x2,y2), col_bg2, rounding_corners_flags_cell ? rounding : 0.0f, rounding_corners_flags_cell); + } + } + } + else + { + window->DrawList->AddRectFilled(p_min, p_max, col, rounding, rounding_corners_flags); + } +} + +void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiColorEditFlags__InputsMask) == 0) + flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputsMask; + if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0) + flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask; + if ((flags & ImGuiColorEditFlags__PickerMask) == 0) + flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask; + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__InputsMask))); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__DataTypeMask))); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check only 1 option is selected + g.ColorEditOptions = flags; +} + +// A little colored square. Return true when clicked. +// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. +// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. +bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(desc_id); + float default_size = GetFrameHeight(); + if (size.x == 0.0f) + size.x = default_size; + if (size.y == 0.0f) + size.y = default_size; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + if (flags & ImGuiColorEditFlags_NoAlpha) + flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); + + ImVec4 col_without_alpha(col.x, col.y, col.z, 1.0f); + float grid_step = ImMin(size.x, size.y) / 2.99f; + float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); + ImRect bb_inner = bb; + float off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. + bb_inner.Expand(off); + if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col.w < 1.0f) + { + float mid_x = (float)(int)((bb_inner.Min.x + bb_inner.Max.x) * 0.5f + 0.5f); + RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawCornerFlags_TopRight| ImDrawCornerFlags_BotRight); + window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_without_alpha), rounding, ImDrawCornerFlags_TopLeft|ImDrawCornerFlags_BotLeft); + } + else + { + // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha + ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col : col_without_alpha; + if (col_source.w < 1.0f) + RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); + else + window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding, ImDrawCornerFlags_All); + } + RenderNavHighlight(bb, id); + if (g.Style.FrameBorderSize > 0.0f) + RenderFrameBorder(bb.Min, bb.Max, rounding); + else + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border + + // Drag and Drop Source + if (g.ActiveId == id && BeginDragDropSource()) // NB: The ActiveId test is merely an optional micro-optimization + { + if (flags & ImGuiColorEditFlags_NoAlpha) + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col, sizeof(float) * 3, ImGuiCond_Once); + else + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col, sizeof(float) * 4, ImGuiCond_Once); + ColorButton(desc_id, col, flags); + SameLine(); + TextUnformatted("Color"); + EndDragDropSource(); + hovered = false; + } + + // Tooltip + if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) + ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); + + return pressed; +} + +bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); +} + +void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) +{ + bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__InputsMask); + bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask); + if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) + return; + ImGuiContext& g = *GImGui; + ImGuiColorEditFlags opts = g.ColorEditOptions; + if (allow_opt_inputs) + { + if (RadioButton("RGB", (opts & ImGuiColorEditFlags_RGB) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_RGB; + if (RadioButton("HSV", (opts & ImGuiColorEditFlags_HSV) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HSV; + if (RadioButton("HEX", (opts & ImGuiColorEditFlags_HEX) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HEX; + } + if (allow_opt_datatype) + { + if (allow_opt_inputs) Separator(); + if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Uint8; + if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Float; + } + + if (allow_opt_inputs || allow_opt_datatype) + Separator(); + if (Button("Copy as..", ImVec2(-1,0))) + OpenPopup("Copy"); + if (BeginPopup("Copy")) + { + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + char buf[64]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if (Selectable(buf)) + SetClipboardText(buf); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + if (flags & ImGuiColorEditFlags_NoAlpha) + ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X", cr, cg, cb); + else + ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X%02X", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + EndPopup(); + } + + g.ColorEditOptions = opts; + EndPopup(); +} + +static void ColorPickerOptionsPopup(ImGuiColorEditFlags flags, const float* ref_col) +{ + bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask); + bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); + if ((!allow_opt_picker && !allow_opt_alpha_bar) || !ImGui::BeginPopup("context")) + return; + ImGuiContext& g = *GImGui; + if (allow_opt_picker) + { + ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function + ImGui::PushItemWidth(picker_size.x); + for (int picker_type = 0; picker_type < 2; picker_type++) + { + // Draw small/thumbnail version of each picker type (over an invisible button for selection) + if (picker_type > 0) ImGui::Separator(); + ImGui::PushID(picker_type); + ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoOptions|ImGuiColorEditFlags_NoLabel|ImGuiColorEditFlags_NoSidePreview|(flags & ImGuiColorEditFlags_NoAlpha); + if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; + if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; + ImVec2 backup_pos = ImGui::GetCursorScreenPos(); + if (ImGui::Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup + g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask); + ImGui::SetCursorScreenPos(backup_pos); + ImVec4 dummy_ref_col; + memcpy(&dummy_ref_col.x, ref_col, sizeof(float) * (picker_flags & ImGuiColorEditFlags_NoAlpha ? 3 : 4)); + ImGui::ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags); + ImGui::PopID(); + } + ImGui::PopItemWidth(); + } + if (allow_opt_alpha_bar) + { + if (allow_opt_picker) ImGui::Separator(); + ImGui::CheckboxFlags("Alpha Bar", (unsigned int*)&g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); + } + ImGui::EndPopup(); +} + +// Edit colors components (each component in 0.0f..1.0f range). +// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// With typical options: Left-click on colored square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item. +bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float square_sz = GetFrameHeight(); + const float w_extra = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); + const float w_items_all = CalcItemWidth() - w_extra; + const char* label_display_end = FindRenderedTextEnd(label); + + const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; + const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; + const int components = alpha ? 4 : 3; + const ImGuiColorEditFlags flags_untouched = flags; + + BeginGroup(); + PushID(label); + + // If we're not showing any slider there's no point in doing any HSV conversions + if (flags & ImGuiColorEditFlags_NoInputs) + flags = (flags & (~ImGuiColorEditFlags__InputsMask)) | ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_NoOptions; + + // Context menu: display and modify options (before defaults are applied) + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorEditOptionsPopup(col, flags); + + // Read stored options + if (!(flags & ImGuiColorEditFlags__InputsMask)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputsMask); + if (!(flags & ImGuiColorEditFlags__DataTypeMask)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask); + if (!(flags & ImGuiColorEditFlags__PickerMask)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask); + flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask)); + + // Convert to the formats we need + float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; + if (flags & ImGuiColorEditFlags_HSV) + ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); + int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) }; + + bool value_changed = false; + bool value_changed_as_float = false; + + if ((flags & (ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_HSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB/HSV 0..255 Sliders + const float w_item_one = ImMax(1.0f, (float)(int)((w_items_all - (style.ItemInnerSpacing.x) * (components-1)) / (float)components)); + const float w_item_last = ImMax(1.0f, (float)(int)(w_items_all - (w_item_one + style.ItemInnerSpacing.x) * (components-1))); + + const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); + const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; + const char* fmt_table_int[3][4] = + { + { "%3.0f", "%3.0f", "%3.0f", "%3.0f" }, // Short display + { "R:%3.0f", "G:%3.0f", "B:%3.0f", "A:%3.0f" }, // Long display for RGBA + { "H:%3.0f", "S:%3.0f", "V:%3.0f", "A:%3.0f" } // Long display for HSVA + }; + const char* fmt_table_float[3][4] = + { + { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display + { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA + { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA + }; + const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_HSV) ? 2 : 1; + + PushItemWidth(w_item_one); + for (int n = 0; n < components; n++) + { + if (n > 0) + SameLine(0, style.ItemInnerSpacing.x); + if (n + 1 == components) + PushItemWidth(w_item_last); + if (flags & ImGuiColorEditFlags_Float) + value_changed = value_changed_as_float = value_changed | DragFloat(ids[n], &f[n], 1.0f/255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); + else + value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + } + PopItemWidth(); + PopItemWidth(); + } + else if ((flags & ImGuiColorEditFlags_HEX) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB Hexadecimal Input + char buf[64]; + if (alpha) + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255), ImClamp(i[3],0,255)); + else + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255)); + PushItemWidth(w_items_all); + if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) + { + value_changed = true; + char* p = buf; + while (*p == '#' || ImCharIsSpace(*p)) + p++; + i[0] = i[1] = i[2] = i[3] = 0; + if (alpha) + sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) + else + sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + PopItemWidth(); + } + + ImGuiWindow* picker_active_window = NULL; + if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) + { + if (!(flags & ImGuiColorEditFlags_NoInputs)) + SameLine(0, style.ItemInnerSpacing.x); + + const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); + if (ColorButton("##ColorButton", col_v4, flags)) + { + if (!(flags & ImGuiColorEditFlags_NoPicker)) + { + // Store current color and open a picker + g.ColorPickerRef = col_v4; + OpenPopup("picker"); + SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1,style.ItemSpacing.y)); + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + + if (BeginPopup("picker")) + { + picker_active_window = g.CurrentWindow; + if (label != label_display_end) + { + TextUnformatted(label, label_display_end); + Separator(); + } + ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; + ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; + PushItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? + value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); + PopItemWidth(); + EndPopup(); + } + } + + if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) + { + SameLine(0, style.ItemInnerSpacing.x); + TextUnformatted(label, label_display_end); + } + + // Convert back + if (picker_active_window == NULL) + { + if (!value_changed_as_float) + for (int n = 0; n < 4; n++) + f[n] = i[n] / 255.0f; + if (flags & ImGuiColorEditFlags_HSV) + ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); + if (value_changed) + { + col[0] = f[0]; + col[1] = f[1]; + col[2] = f[2]; + if (alpha) + col[3] = f[3]; + } + } + + PopID(); + EndGroup(); + + // Drag and Drop Target + if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && BeginDragDropTarget()) // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. + { + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * 3); + value_changed = true; + } + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * components); + value_changed = true; + } + EndDragDropTarget(); + } + + // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). + if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) + window->DC.LastItemId = g.ActiveId; + + return value_changed; +} + +bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + float col4[4] = { col[0], col[1], col[2], 1.0f }; + if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) + return false; + col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2]; + return true; +} + +// 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. +static void RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col) +{ + switch (direction) + { + case ImGuiDir_Left: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Right: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_Up: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Down: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_None: case ImGuiDir_Count_: break; // Fix warnings + } +} + +static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w) +{ + RenderArrow(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32_BLACK); + RenderArrow(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32_WHITE); + RenderArrow(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32_BLACK); + RenderArrow(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32_WHITE); +} + +// ColorPicker +// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..) +bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImDrawList* draw_list = window->DrawList; + + ImGuiStyle& style = g.Style; + ImGuiIO& io = g.IO; + + PushID(label); + BeginGroup(); + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + flags |= ImGuiColorEditFlags_NoSmallPreview; + + // Context menu: display and store options. + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorPickerOptionsPopup(flags, col); + + // Read stored options + if (!(flags & ImGuiColorEditFlags__PickerMask)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask; + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check that only 1 is selected + if (!(flags & ImGuiColorEditFlags_NoOptions)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); + + // Setup + int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; + bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); + ImVec2 picker_pos = window->DC.CursorPos; + float square_sz = GetFrameHeight(); + float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars + float sv_picker_size = ImMax(bars_width * 1, CalcItemWidth() - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box + float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; + float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; + float bars_triangles_half_sz = (float)(int)(bars_width * 0.20f); + + float backup_initial_col[4]; + memcpy(backup_initial_col, col, components * sizeof(float)); + + float wheel_thickness = sv_picker_size * 0.08f; + float wheel_r_outer = sv_picker_size * 0.50f; + float wheel_r_inner = wheel_r_outer - wheel_thickness; + ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size*0.5f); + + // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. + float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); + ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. + ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. + ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. + + float H,S,V; + ColorConvertRGBtoHSV(col[0], col[1], col[2], H, S, V); + + bool value_changed = false, value_changed_h = false, value_changed_sv = false; + + PushItemFlag(ImGuiItemFlags_NoNav, true); + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Hue wheel + SV triangle logic + InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); + if (IsItemActive()) + { + ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; + ImVec2 current_off = g.IO.MousePos - wheel_center; + float initial_dist2 = ImLengthSqr(initial_off); + if (initial_dist2 >= (wheel_r_inner-1)*(wheel_r_inner-1) && initial_dist2 <= (wheel_r_outer+1)*(wheel_r_outer+1)) + { + // Interactive with Hue wheel + H = atan2f(current_off.y, current_off.x) / IM_PI*0.5f; + if (H < 0.0f) + H += 1.0f; + value_changed = value_changed_h = true; + } + float cos_hue_angle = cosf(-H * 2.0f * IM_PI); + float sin_hue_angle = sinf(-H * 2.0f * IM_PI); + if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) + { + // Interacting with SV triangle + ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); + if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) + current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); + float uu, vv, ww; + ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); + V = ImClamp(1.0f - vv, 0.0001f, 1.0f); + S = ImClamp(uu / V, 0.0001f, 1.0f); + value_changed = value_changed_sv = true; + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // SV rectangle logic + InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); + if (IsItemActive()) + { + S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size-1)); + V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + value_changed = value_changed_sv = true; + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + + // Hue bar logic + SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); + InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + value_changed = value_changed_h = true; + } + } + + // Alpha bar logic + if (alpha_bar) + { + SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); + InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + value_changed = true; + } + } + PopItemFlag(); // ImGuiItemFlags_NoNav + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + SameLine(0, style.ItemInnerSpacing.x); + BeginGroup(); + } + + if (!(flags & ImGuiColorEditFlags_NoLabel)) + { + const char* label_display_end = FindRenderedTextEnd(label); + if (label != label_display_end) + { + if ((flags & ImGuiColorEditFlags_NoSidePreview)) + SameLine(0, style.ItemInnerSpacing.x); + TextUnformatted(label, label_display_end); + } + } + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); + ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if ((flags & ImGuiColorEditFlags_NoLabel)) + Text("Current"); + ColorButton("##current", col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2)); + if (ref_col != NULL) + { + Text("Original"); + ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); + if (ColorButton("##original", ref_col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2))) + { + memcpy(col, ref_col, components * sizeof(float)); + value_changed = true; + } + } + PopItemFlag(); + EndGroup(); + } + + // Convert back color to RGB + if (value_changed_h || value_changed_sv) + ColorConvertHSVtoRGB(H >= 1.0f ? H - 10 * 1e-6f : H, S > 0.0f ? S : 10*1e-6f, V > 0.0f ? V : 1e-6f, col[0], col[1], col[2]); + + // R,G,B and H,S,V slider color editor + if ((flags & ImGuiColorEditFlags_NoInputs) == 0) + { + PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; + ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; + if (flags & ImGuiColorEditFlags_RGB || (flags & ImGuiColorEditFlags__InputsMask) == 0) + value_changed |= ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_RGB); + if (flags & ImGuiColorEditFlags_HSV || (flags & ImGuiColorEditFlags__InputsMask) == 0) + value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_HSV); + if (flags & ImGuiColorEditFlags_HEX || (flags & ImGuiColorEditFlags__InputsMask) == 0) + value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_HEX); + PopItemWidth(); + } + + // Try to cancel hue wrap (after ColorEdit), if any + if (value_changed) + { + float new_H, new_S, new_V; + ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); + if (new_H <= 0 && H > 0) + { + if (new_V <= 0 && V != new_V) + ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); + else if (new_S <= 0) + ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); + } + } + + ImVec4 hue_color_f(1, 1, 1, 1); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); + ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); + ImU32 col32_no_alpha = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 1.0f)); + + const ImU32 hue_colors[6+1] = { IM_COL32(255,0,0,255), IM_COL32(255,255,0,255), IM_COL32(0,255,0,255), IM_COL32(0,255,255,255), IM_COL32(0,0,255,255), IM_COL32(255,0,255,255), IM_COL32(255,0,0,255) }; + ImVec2 sv_cursor_pos; + + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Render Hue Wheel + const float aeps = 1.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). + const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12); + for (int n = 0; n < 6; n++) + { + const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps; + const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; + const int vert_start_idx = draw_list->VtxBuffer.Size; + draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); + draw_list->PathStroke(IM_COL32_WHITE, false, wheel_thickness); + const int vert_end_idx = draw_list->VtxBuffer.Size; + + // Paint colors over existing vertices + ImVec2 gradient_p0(wheel_center.x + cosf(a0) * wheel_r_inner, wheel_center.y + sinf(a0) * wheel_r_inner); + ImVec2 gradient_p1(wheel_center.x + cosf(a1) * wheel_r_inner, wheel_center.y + sinf(a1) * wheel_r_inner); + ShadeVertsLinearColorGradientKeepAlpha(draw_list->VtxBuffer.Data + vert_start_idx, draw_list->VtxBuffer.Data + vert_end_idx, gradient_p0, gradient_p1, hue_colors[n], hue_colors[n+1]); + } + + // Render Cursor + preview on Hue Wheel + float cos_hue_angle = cosf(H * 2.0f * IM_PI); + float sin_hue_angle = sinf(H * 2.0f * IM_PI); + ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f); + float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; + int hue_cursor_segments = ImClamp((int)(hue_cursor_rad / 1.4f), 9, 32); + draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad+1, IM_COL32(128,128,128,255), hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, IM_COL32_WHITE, hue_cursor_segments); + + // Render SV triangle (rotated according to hue) + ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); + ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); + ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); + ImVec2 uv_white = GetFontTexUvWhitePixel(); + draw_list->PrimReserve(6, 6); + draw_list->PrimVtx(tra, uv_white, hue_color32); + draw_list->PrimVtx(trb, uv_white, hue_color32); + draw_list->PrimVtx(trc, uv_white, IM_COL32_WHITE); + draw_list->PrimVtx(tra, uv_white, IM_COL32_BLACK_TRANS); + draw_list->PrimVtx(trb, uv_white, IM_COL32_BLACK); + draw_list->PrimVtx(trc, uv_white, IM_COL32_BLACK_TRANS); + draw_list->AddTriangle(tra, trb, trc, IM_COL32(128,128,128,255), 1.5f); + sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // Render SV Square + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_WHITE, hue_color32, hue_color32, IM_COL32_WHITE); + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32_BLACK, IM_COL32_BLACK); + RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), 0.0f); + sv_cursor_pos.x = ImClamp((float)(int)(picker_pos.x + ImSaturate(S) * sv_picker_size + 0.5f), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much + sv_cursor_pos.y = ImClamp((float)(int)(picker_pos.y + ImSaturate(1 - V) * sv_picker_size + 0.5f), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); + + // Render Hue Bar + for (int i = 0; i < 6; ++i) + draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), hue_colors[i], hue_colors[i], hue_colors[i + 1], hue_colors[i + 1]); + float bar0_line_y = (float)(int)(picker_pos.y + H * sv_picker_size + 0.5f); + RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f); + } + + // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) + float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; + draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, col32_no_alpha, 12); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad+1, IM_COL32(128,128,128,255), 12); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, IM_COL32_WHITE, 12); + + // Render alpha bar + if (alpha_bar) + { + float alpha = ImSaturate(col[3]); + ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); + RenderColorRectWithAlphaCheckerboard(bar1_bb.Min, bar1_bb.Max, IM_COL32(0,0,0,0), bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); + draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, col32_no_alpha, col32_no_alpha, col32_no_alpha & ~IM_COL32_A_MASK, col32_no_alpha & ~IM_COL32_A_MASK); + float bar1_line_y = (float)(int)(picker_pos.y + (1.0f - alpha) * sv_picker_size + 0.5f); + RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f); + } + + EndGroup(); + PopID(); + + return value_changed && memcmp(backup_initial_col, col, components * sizeof(float)); +} + +// Horizontal separating line. +void ImGui::Separator() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + ImGuiWindowFlags flags = 0; + if ((flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical)) == 0) + flags |= (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; + IM_ASSERT(ImIsPowerOfTwo((int)(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical)))); // Check that only 1 option is selected + if (flags & ImGuiSeparatorFlags_Vertical) + { + VerticalSeparator(); + return; + } + + // Horizontal Separator + if (window->DC.ColumnsSet) + PopClipRect(); + + float x1 = window->Pos.x; + float x2 = window->Pos.x + window->Size.x; + if (!window->DC.GroupStack.empty()) + x1 += window->DC.IndentX; + + const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y+1.0f)); + ItemSize(ImVec2(0.0f, 0.0f)); // NB: we don't provide our width so that it doesn't get feed back into AutoFit, we don't provide height to not alter layout. + if (!ItemAdd(bb, 0)) + { + if (window->DC.ColumnsSet) + PushColumnClipRect(); + return; + } + + window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x,bb.Min.y), GetColorU32(ImGuiCol_Separator)); + + if (g.LogEnabled) + LogRenderedText(NULL, IM_NEWLINE "--------------------------------"); + + if (window->DC.ColumnsSet) + { + PushColumnClipRect(); + window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; + } +} + +void ImGui::VerticalSeparator() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + float y1 = window->DC.CursorPos.y; + float y2 = window->DC.CursorPos.y + window->DC.CurrentLineHeight; + const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + 1.0f, y2)); + ItemSize(ImVec2(bb.GetWidth(), 0.0f)); + if (!ItemAdd(bb, 0)) + return; + + window->DrawList->AddLine(ImVec2(bb.Min.x, bb.Min.y), ImVec2(bb.Min.x, bb.Max.y), GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogText(" |"); +} + +bool ImGui::SplitterBehavior(ImGuiID id, const ImRect& bb, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; + window->DC.ItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus; + bool item_add = ItemAdd(bb, id); + window->DC.ItemFlags = item_flags_backup; + if (!item_add) + return false; + + bool hovered, held; + ImRect bb_interact = bb; + bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); + ButtonBehavior(bb_interact, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); + if (g.ActiveId != id) + SetItemAllowOverlap(); + + if (held || (g.HoveredId == id && g.HoveredIdPreviousFrame == id)) + SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + + ImRect bb_render = bb; + if (held) + { + ImVec2 mouse_delta_2d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min; + float mouse_delta = (axis == ImGuiAxis_Y) ? mouse_delta_2d.y : mouse_delta_2d.x; + + // Minimum pane size + if (mouse_delta < min_size1 - *size1) + mouse_delta = min_size1 - *size1; + if (mouse_delta > *size2 - min_size2) + mouse_delta = *size2 - min_size2; + + // Apply resize + *size1 += mouse_delta; + *size2 -= mouse_delta; + bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); + } + + // Render + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, g.Style.FrameRounding); + + return held; +} + +void ImGui::Spacing() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ItemSize(ImVec2(0,0)); +} + +void ImGui::Dummy(const ImVec2& size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb); + ItemAdd(bb, 0); +} + +bool ImGui::IsRectVisible(const ImVec2& size) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); +} + +bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); +} + +// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) +void ImGui::BeginGroup() +{ + ImGuiWindow* window = GetCurrentWindow(); + + window->DC.GroupStack.resize(window->DC.GroupStack.Size + 1); + ImGuiGroupData& group_data = window->DC.GroupStack.back(); + group_data.BackupCursorPos = window->DC.CursorPos; + group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; + group_data.BackupIndentX = window->DC.IndentX; + group_data.BackupGroupOffsetX = window->DC.GroupOffsetX; + group_data.BackupCurrentLineHeight = window->DC.CurrentLineHeight; + group_data.BackupCurrentLineTextBaseOffset = window->DC.CurrentLineTextBaseOffset; + group_data.BackupLogLinePosY = window->DC.LogLinePosY; + group_data.BackupActiveIdIsAlive = GImGui->ActiveIdIsAlive; + group_data.AdvanceCursor = true; + + window->DC.GroupOffsetX = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffsetX; + window->DC.IndentX = window->DC.GroupOffsetX; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.CurrentLineHeight = 0.0f; + window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; +} + +void ImGui::EndGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + IM_ASSERT(!window->DC.GroupStack.empty()); // Mismatched BeginGroup()/EndGroup() calls + + ImGuiGroupData& group_data = window->DC.GroupStack.back(); + + ImRect group_bb(group_data.BackupCursorPos, window->DC.CursorMaxPos); + group_bb.Max = ImMax(group_bb.Min, group_bb.Max); + + window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, window->DC.CursorMaxPos); + window->DC.CurrentLineHeight = group_data.BackupCurrentLineHeight; + window->DC.CurrentLineTextBaseOffset = group_data.BackupCurrentLineTextBaseOffset; + window->DC.IndentX = group_data.BackupIndentX; + window->DC.GroupOffsetX = group_data.BackupGroupOffsetX; + window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; + + if (group_data.AdvanceCursor) + { + window->DC.CurrentLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrentLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. + ItemSize(group_bb.GetSize(), group_data.BackupCurrentLineTextBaseOffset); + ItemAdd(group_bb, 0); + } + + // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive() will be functional on the entire group. + // It would be be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but if you search for LastItemId you'll notice it is only used in that context. + const bool active_id_within_group = (!group_data.BackupActiveIdIsAlive && g.ActiveIdIsAlive && g.ActiveId && g.ActiveIdWindow->RootWindow == window->RootWindow); + if (active_id_within_group) + window->DC.LastItemId = g.ActiveId; + window->DC.LastItemRect = group_bb; + + window->DC.GroupStack.pop_back(); + + //window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] +} + +// Gets back to previous line and continue with horizontal layout +// pos_x == 0 : follow right after previous item +// pos_x != 0 : align to specified x position (relative to window/group left) +// spacing_w < 0 : use default spacing if pos_x == 0, no spacing if pos_x != 0 +// spacing_w >= 0 : enforce spacing amount +void ImGui::SameLine(float pos_x, float spacing_w) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + if (pos_x != 0.0f) + { + if (spacing_w < 0.0f) spacing_w = 0.0f; + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + pos_x + spacing_w + window->DC.GroupOffsetX + window->DC.ColumnsOffsetX; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + else + { + if (spacing_w < 0.0f) spacing_w = g.Style.ItemSpacing.x; + window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + window->DC.CurrentLineHeight = window->DC.PrevLineHeight; + window->DC.CurrentLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; +} + +void ImGui::NewLine() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiLayoutType backup_layout_type = window->DC.LayoutType; + window->DC.LayoutType = ImGuiLayoutType_Vertical; + if (window->DC.CurrentLineHeight > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height. + ItemSize(ImVec2(0,0)); + else + ItemSize(ImVec2(0.0f, g.FontSize)); + window->DC.LayoutType = backup_layout_type; +} + +void ImGui::NextColumn() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems || window->DC.ColumnsSet == NULL) + return; + + ImGuiContext& g = *GImGui; + PopItemWidth(); + PopClipRect(); + + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + columns->CellMaxY = ImMax(columns->CellMaxY, window->DC.CursorPos.y); + if (++columns->Current < columns->Count) + { + // Columns 1+ cancel out IndentX + window->DC.ColumnsOffsetX = GetColumnOffset(columns->Current) - window->DC.IndentX + g.Style.ItemSpacing.x; + window->DrawList->ChannelsSetCurrent(columns->Current); + } + else + { + window->DC.ColumnsOffsetX = 0.0f; + window->DrawList->ChannelsSetCurrent(0); + columns->Current = 0; + columns->CellMinY = columns->CellMaxY; + } + window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); + window->DC.CursorPos.y = columns->CellMinY; + window->DC.CurrentLineHeight = 0.0f; + window->DC.CurrentLineTextBaseOffset = 0.0f; + + PushColumnClipRect(); + PushItemWidth(GetColumnWidth() * 0.65f); // FIXME: Move on columns setup +} + +int ImGui::GetColumnIndex() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.ColumnsSet ? window->DC.ColumnsSet->Current : 0; +} + +int ImGui::GetColumnsCount() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.ColumnsSet ? window->DC.ColumnsSet->Count : 1; +} + +static float OffsetNormToPixels(const ImGuiColumnsSet* columns, float offset_norm) +{ + return offset_norm * (columns->MaxX - columns->MinX); +} + +static float PixelsToOffsetNorm(const ImGuiColumnsSet* columns, float offset) +{ + return offset / (columns->MaxX - columns->MinX); +} + +static inline float GetColumnsRectHalfWidth() { return 4.0f; } + +static float GetDraggedColumnOffset(ImGuiColumnsSet* columns, int column_index) +{ + // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing + // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(column_index > 0); // We cannot drag column 0. If you get this assert you may have a conflict between the ID of your columns and another widgets. + IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index)); + + float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + GetColumnsRectHalfWidth() - window->Pos.x; + x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing); + if ((columns->Flags & ImGuiColumnsFlags_NoPreserveWidths)) + x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing); + + return x; +} + +float ImGui::GetColumnOffset(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + /* + if (g.ActiveId) + { + ImGuiContext& g = *GImGui; + const ImGuiID column_id = columns->ColumnsSetId + ImGuiID(column_index); + if (g.ActiveId == column_id) + return GetDraggedColumnOffset(columns, column_index); + } + */ + + const float t = columns->Columns[column_index].OffsetNorm; + const float x_offset = ImLerp(columns->MinX, columns->MaxX, t); + return x_offset; +} + +static float GetColumnWidthEx(ImGuiColumnsSet* columns, int column_index, bool before_resize = false) +{ + if (column_index < 0) + column_index = columns->Current; + + float offset_norm; + if (before_resize) + offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize; + else + offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm; + return OffsetNormToPixels(columns, offset_norm); +} + +float ImGui::GetColumnWidth(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + return OffsetNormToPixels(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); +} + +void ImGui::SetColumnOffset(int column_index, float offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const bool preserve_width = !(columns->Flags & ImGuiColumnsFlags_NoPreserveWidths) && (column_index < columns->Count-1); + const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f; + + if (!(columns->Flags & ImGuiColumnsFlags_NoForceWithinWindow)) + offset = ImMin(offset, columns->MaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index)); + columns->Columns[column_index].OffsetNorm = PixelsToOffsetNorm(columns, offset - columns->MinX); + + if (preserve_width) + SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); +} + +void ImGui::SetColumnWidth(int column_index, float width) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); +} + +void ImGui::PushColumnClipRect(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + if (column_index < 0) + column_index = columns->Current; + + PushClipRect(columns->Columns[column_index].ClipRect.Min, columns->Columns[column_index].ClipRect.Max, false); +} + +static ImGuiColumnsSet* FindOrAddColumnsSet(ImGuiWindow* window, ImGuiID id) +{ + for (int n = 0; n < window->ColumnsStorage.Size; n++) + if (window->ColumnsStorage[n].ID == id) + return &window->ColumnsStorage[n]; + + window->ColumnsStorage.push_back(ImGuiColumnsSet()); + ImGuiColumnsSet* columns = &window->ColumnsStorage.back(); + columns->ID = id; + return columns; +} + +void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiColumnsFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + IM_ASSERT(columns_count > 1); + IM_ASSERT(window->DC.ColumnsSet == NULL); // Nested columns are currently not supported + + // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget. + // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer. + PushID(0x11223347 + (str_id ? 0 : columns_count)); + ImGuiID id = window->GetID(str_id ? str_id : "columns"); + PopID(); + + // Acquire storage for the columns set + ImGuiColumnsSet* columns = FindOrAddColumnsSet(window, id); + IM_ASSERT(columns->ID == id); + columns->Current = 0; + columns->Count = columns_count; + columns->Flags = flags; + window->DC.ColumnsSet = columns; + + // Set state for first column + const float content_region_width = (window->SizeContentsExplicit.x != 0.0f) ? (window->SizeContentsExplicit.x) : (window->Size.x -window->ScrollbarSizes.x); + columns->MinX = window->DC.IndentX - g.Style.ItemSpacing.x; // Lock our horizontal range + //column->MaxX = content_region_width - window->Scroll.x - ((window->Flags & ImGuiWindowFlags_NoScrollbar) ? 0 : g.Style.ScrollbarSize);// - window->WindowPadding().x; + columns->MaxX = content_region_width - window->Scroll.x; + columns->StartPosY = window->DC.CursorPos.y; + columns->StartMaxPosX = window->DC.CursorMaxPos.x; + columns->CellMinY = columns->CellMaxY = window->DC.CursorPos.y; + window->DC.ColumnsOffsetX = 0.0f; + window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); + + // Clear data if columns count changed + if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1) + columns->Columns.resize(0); + + // Initialize defaults + columns->IsFirstFrame = (columns->Columns.Size == 0); + if (columns->Columns.Size == 0) + { + columns->Columns.reserve(columns_count + 1); + for (int n = 0; n < columns_count + 1; n++) + { + ImGuiColumnData column; + column.OffsetNorm = n / (float)columns_count; + columns->Columns.push_back(column); + } + } + + for (int n = 0; n < columns_count + 1; n++) + { + // Clamp position + ImGuiColumnData* column = &columns->Columns[n]; + float t = column->OffsetNorm; + if (!(columns->Flags & ImGuiColumnsFlags_NoForceWithinWindow)) + t = ImMin(t, PixelsToOffsetNorm(columns, (columns->MaxX - columns->MinX) - g.Style.ColumnsMinSpacing * (columns->Count - n))); + column->OffsetNorm = t; + + if (n == columns_count) + continue; + + // Compute clipping rectangle + float clip_x1 = ImFloor(0.5f + window->Pos.x + GetColumnOffset(n) - 1.0f); + float clip_x2 = ImFloor(0.5f + window->Pos.x + GetColumnOffset(n + 1) - 1.0f); + column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX); + column->ClipRect.ClipWith(window->ClipRect); + } + + window->DrawList->ChannelsSplit(columns->Count); + PushColumnClipRect(); + PushItemWidth(GetColumnWidth() * 0.65f); +} + +void ImGui::EndColumns() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); + + PopItemWidth(); + PopClipRect(); + window->DrawList->ChannelsMerge(); + + columns->CellMaxY = ImMax(columns->CellMaxY, window->DC.CursorPos.y); + window->DC.CursorPos.y = columns->CellMaxY; + if (!(columns->Flags & ImGuiColumnsFlags_GrowParentContentsSize)) + window->DC.CursorMaxPos.x = ImMax(columns->StartMaxPosX, columns->MaxX); // Restore cursor max pos, as columns don't grow parent + + // Draw columns borders and handle resize + bool is_being_resized = false; + if (!(columns->Flags & ImGuiColumnsFlags_NoBorder) && !window->SkipItems) + { + const float y1 = columns->StartPosY; + const float y2 = window->DC.CursorPos.y; + int dragging_column = -1; + for (int n = 1; n < columns->Count; n++) + { + float x = window->Pos.x + GetColumnOffset(n); + const ImGuiID column_id = columns->ID + ImGuiID(n); + const float column_hw = GetColumnsRectHalfWidth(); // Half-width for interaction + const ImRect column_rect(ImVec2(x - column_hw, y1), ImVec2(x + column_hw, y2)); + KeepAliveID(column_id); + if (IsClippedEx(column_rect, column_id, false)) + continue; + + bool hovered = false, held = false; + if (!(columns->Flags & ImGuiColumnsFlags_NoResize)) + { + ButtonBehavior(column_rect, column_id, &hovered, &held); + if (hovered || held) + g.MouseCursor = ImGuiMouseCursor_ResizeEW; + if (held && !(columns->Columns[n].Flags & ImGuiColumnsFlags_NoResize)) + dragging_column = n; + } + + // Draw column (we clip the Y boundaries CPU side because very long triangles are mishandled by some GPU drivers.) + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + const float xi = (float)(int)x; + window->DrawList->AddLine(ImVec2(xi, ImMax(y1 + 1.0f, window->ClipRect.Min.y)), ImVec2(xi, ImMin(y2, window->ClipRect.Max.y)), col); + } + + // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. + if (dragging_column != -1) + { + if (!columns->IsBeingResized) + for (int n = 0; n < columns->Count + 1; n++) + columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; + columns->IsBeingResized = is_being_resized = true; + float x = GetDraggedColumnOffset(columns, dragging_column); + SetColumnOffset(dragging_column, x); + } + } + columns->IsBeingResized = is_being_resized; + + window->DC.ColumnsSet = NULL; + window->DC.ColumnsOffsetX = 0.0f; + window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); +} + +// [2017/12: This is currently the only public API, while we are working on making BeginColumns/EndColumns user-facing] +void ImGui::Columns(int columns_count, const char* id, bool border) +{ + ImGuiWindow* window = GetCurrentWindow(); + IM_ASSERT(columns_count >= 1); + if (window->DC.ColumnsSet != NULL && window->DC.ColumnsSet->Count != columns_count) + EndColumns(); + + ImGuiColumnsFlags flags = (border ? 0 : ImGuiColumnsFlags_NoBorder); + //flags |= ImGuiColumnsFlags_NoPreserveWidths; // NB: Legacy behavior + if (columns_count != 1) + BeginColumns(id, columns_count, flags); +} + +void ImGui::Indent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.IndentX += (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX; +} + +void ImGui::Unindent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.IndentX -= (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX; +} + +void ImGui::TreePush(const char* str_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(str_id ? str_id : "#TreePush"); +} + +void ImGui::TreePush(const void* ptr_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(ptr_id ? ptr_id : (const void*)"#TreePush"); +} + +void ImGui::TreePushRawID(ImGuiID id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + window->IDStack.push_back(id); +} + +void ImGui::TreePop() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + Unindent(); + + window->DC.TreeDepth--; + if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + if (g.NavIdIsAlive && (window->DC.TreeDepthMayJumpToParentOnPop & (1 << window->DC.TreeDepth))) + { + SetNavID(window->IDStack.back(), g.NavLayer); + NavMoveRequestCancel(); + } + window->DC.TreeDepthMayJumpToParentOnPop &= (1 << window->DC.TreeDepth) - 1; + + PopID(); +} + +void ImGui::Value(const char* prefix, bool b) +{ + Text("%s: %s", prefix, (b ? "true" : "false")); +} + +void ImGui::Value(const char* prefix, int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, unsigned int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, float v, const char* float_format) +{ + if (float_format) + { + char fmt[64]; + ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format); + Text(fmt, prefix, v); + } + else + { + Text("%s: %.3f", prefix, v); + } +} + +//----------------------------------------------------------------------------- +// DRAG AND DROP +//----------------------------------------------------------------------------- + +void ImGui::ClearDragDrop() +{ + ImGuiContext& g = *GImGui; + g.DragDropActive = false; + g.DragDropPayload.Clear(); + g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropAcceptFrameCount = -1; +} + +// Call when current ID is active. +// When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() +bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + bool source_drag_active = false; + ImGuiID source_id = 0; + ImGuiID source_parent_id = 0; + int mouse_button = 0; + if (!(flags & ImGuiDragDropFlags_SourceExtern)) + { + source_id = window->DC.LastItemId; + if (source_id != 0 && g.ActiveId != source_id) // Early out for most common case + return false; + if (g.IO.MouseDown[mouse_button] == false) + return false; + + if (source_id == 0) + { + // If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to: + // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag, C) Swallow your programmer pride. + if (!(flags & ImGuiDragDropFlags_SourceAllowNullID)) + { + IM_ASSERT(0); + return false; + } + + // Magic fallback (=somehow reprehensible) to handle items with no assigned ID, e.g. Text(), Image() + // We build a throwaway ID based on current ID stack + relative AABB of items in window. + // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING OF THE WIDGET, so if your widget moves your dragging operation will be canceled. + // We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive. + bool is_hovered = (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) != 0; + if (!is_hovered && (g.ActiveId == 0 || g.ActiveIdWindow != window)) + return false; + source_id = window->DC.LastItemId = window->GetIDFromRectangle(window->DC.LastItemRect); + if (is_hovered) + SetHoveredID(source_id); + if (is_hovered && g.IO.MouseClicked[mouse_button]) + { + SetActiveID(source_id, window); + FocusWindow(window); + } + if (g.ActiveId == source_id) // Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker. + g.ActiveIdAllowOverlap = is_hovered; + } + if (g.ActiveId != source_id) + return false; + source_parent_id = window->IDStack.back(); + source_drag_active = IsMouseDragging(mouse_button); + } + else + { + window = NULL; + source_id = ImHash("#SourceExtern", 0); + source_drag_active = true; + } + + if (source_drag_active) + { + if (!g.DragDropActive) + { + IM_ASSERT(source_id != 0); + ClearDragDrop(); + ImGuiPayload& payload = g.DragDropPayload; + payload.SourceId = source_id; + payload.SourceParentId = source_parent_id; + g.DragDropActive = true; + g.DragDropSourceFlags = flags; + g.DragDropMouseButton = mouse_button; + } + + if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + // FIXME-DRAG + //SetNextWindowPos(g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding); + //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This is better but e.g ColorButton with checkboard has issue with transparent colors :( + SetNextWindowPos(g.IO.MousePos); + PushStyleColor(ImGuiCol_PopupBg, GetStyleColorVec4(ImGuiCol_PopupBg) * ImVec4(1.0f, 1.0f, 1.0f, 0.6f)); + BeginTooltip(); + } + + if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) + window->DC.LastItemStatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; + + return true; + } + return false; +} + +void ImGui::EndDragDropSource() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + + if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + EndTooltip(); + PopStyleColor(); + //PopStyleVar(); + } + + // Discard the drag if have not called SetDragDropPayload() + if (g.DragDropPayload.DataFrameCount == -1) + ClearDragDrop(); +} + +// Use 'cond' to choose to submit payload on drag start or every frame +bool ImGui::SetDragDropPayload(const char* type, const void* data, size_t data_size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + ImGuiPayload& payload = g.DragDropPayload; + if (cond == 0) + cond = ImGuiCond_Always; + + IM_ASSERT(type != NULL); + IM_ASSERT(strlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 12 characters long"); + IM_ASSERT((data != NULL && data_size > 0) || (data == NULL && data_size == 0)); + IM_ASSERT(cond == ImGuiCond_Always || cond == ImGuiCond_Once); + IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() + + if (cond == ImGuiCond_Always || payload.DataFrameCount == -1) + { + // Copy payload + ImStrncpy(payload.DataType, type, IM_ARRAYSIZE(payload.DataType)); + g.DragDropPayloadBufHeap.resize(0); + if (data_size > sizeof(g.DragDropPayloadBufLocal)) + { + // Store in heap + g.DragDropPayloadBufHeap.resize((int)data_size); + payload.Data = g.DragDropPayloadBufHeap.Data; + memcpy((void*)payload.Data, data, data_size); + } + else if (data_size > 0) + { + // Store locally + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); + payload.Data = g.DragDropPayloadBufLocal; + memcpy((void*)payload.Data, data, data_size); + } + else + { + payload.Data = NULL; + } + payload.DataSize = (int)data_size; + } + payload.DataFrameCount = g.FrameCount; + + return (g.DragDropAcceptFrameCount == g.FrameCount) || (g.DragDropAcceptFrameCount == g.FrameCount - 1); +} + +bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) + return false; + IM_ASSERT(id != 0); + if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) + return false; + + g.DragDropTargetRect = bb; + g.DragDropTargetId = id; + return true; +} + +// We don't use BeginDragDropTargetCustom() and duplicate its code because: +// 1) we use LastItemRectHoveredRect which handles items that pushes a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. +// Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) +bool ImGui::BeginDragDropTarget() +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) + return false; + if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) + return false; + + const ImRect& display_rect = (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? window->DC.LastItemDisplayRect : window->DC.LastItemRect; + ImGuiID id = window->DC.LastItemId; + if (id == 0) + id = window->GetIDFromRectangle(display_rect); + if (g.DragDropPayload.SourceId == id) + return false; + + g.DragDropTargetRect = display_rect; + g.DragDropTargetId = id; + return true; +} + +bool ImGui::IsDragDropPayloadBeingAccepted() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive && g.DragDropAcceptIdPrev != 0; +} + +const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiPayload& payload = g.DragDropPayload; + IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? + IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? + if (type != NULL && !payload.IsDataType(type)) + return NULL; + + // Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints. + // NB: We currently accept NULL id as target. However, overlapping targets requires a unique ID to function! + const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); + ImRect r = g.DragDropTargetRect; + float r_surface = r.GetWidth() * r.GetHeight(); + if (r_surface < g.DragDropAcceptIdCurrRectSurface) + { + g.DragDropAcceptIdCurr = g.DragDropTargetId; + g.DragDropAcceptIdCurrRectSurface = r_surface; + } + + // Render default drop visuals + payload.Preview = was_accepted_previously; + flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that lives for 1 frame) + if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) + { + // FIXME-DRAG: Settle on a proper default visuals for drop target. + r.Expand(3.5f); + bool push_clip_rect = !window->ClipRect.Contains(r); + if (push_clip_rect) window->DrawList->PushClipRectFullScreen(); + window->DrawList->AddRect(r.Min, r.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, ~0, 2.0f); + if (push_clip_rect) window->DrawList->PopClipRect(); + } + + g.DragDropAcceptFrameCount = g.FrameCount; + payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting os window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() + if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) + return NULL; + + return &payload; +} + +// We don't really use/need this now, but added it for the sake of consistency and because we might need it later. +void ImGui::EndDragDropTarget() +{ + ImGuiContext& g = *GImGui; (void)g; + IM_ASSERT(g.DragDropActive); +} + +//----------------------------------------------------------------------------- +// PLATFORM DEPENDENT HELPERS +//----------------------------------------------------------------------------- + +#if defined(_WIN32) && !defined(_WINDOWS_) && (!defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) || !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS)) +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#ifndef __MINGW32__ +#include +#else +#include +#endif +#endif + +// Win32 API clipboard implementation +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) + +#ifdef _MSC_VER +#pragma comment(lib, "user32") +#endif + +static const char* GetClipboardTextFn_DefaultImpl(void*) +{ + static ImVector buf_local; + buf_local.clear(); + if (!OpenClipboard(NULL)) + return NULL; + HANDLE wbuf_handle = GetClipboardData(CF_UNICODETEXT); + if (wbuf_handle == NULL) + { + CloseClipboard(); + return NULL; + } + if (ImWchar* wbuf_global = (ImWchar*)GlobalLock(wbuf_handle)) + { + int buf_len = ImTextCountUtf8BytesFromStr(wbuf_global, NULL) + 1; + buf_local.resize(buf_len); + ImTextStrToUtf8(buf_local.Data, buf_len, wbuf_global, NULL); + } + GlobalUnlock(wbuf_handle); + CloseClipboard(); + return buf_local.Data; +} + +static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +{ + if (!OpenClipboard(NULL)) + return; + const int wbuf_length = ImTextCountCharsFromUtf8(text, NULL) + 1; + HGLOBAL wbuf_handle = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)wbuf_length * sizeof(ImWchar)); + if (wbuf_handle == NULL) + { + CloseClipboard(); + return; + } + ImWchar* wbuf_global = (ImWchar*)GlobalLock(wbuf_handle); + ImTextStrFromUtf8(wbuf_global, wbuf_length, text, NULL); + GlobalUnlock(wbuf_handle); + EmptyClipboard(); + SetClipboardData(CF_UNICODETEXT, wbuf_handle); + CloseClipboard(); +} + +#else + +// Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers +static const char* GetClipboardTextFn_DefaultImpl(void*) +{ + ImGuiContext& g = *GImGui; + return g.PrivateClipboard.empty() ? NULL : g.PrivateClipboard.begin(); +} + +// Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers +static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +{ + ImGuiContext& g = *GImGui; + g.PrivateClipboard.clear(); + const char* text_end = text + strlen(text); + g.PrivateClipboard.resize((int)(text_end - text) + 1); + memcpy(&g.PrivateClipboard[0], text, (size_t)(text_end - text)); + g.PrivateClipboard[(int)(text_end - text)] = 0; +} + +#endif + +// Win32 API IME support (for Asian languages, etc.) +#if defined(_WIN32) && !defined(__GNUC__) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) + +#include +#ifdef _MSC_VER +#pragma comment(lib, "imm32") +#endif + +static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y) +{ + // Notify OS Input Method Editor of text input position + if (HWND hwnd = (HWND)GImGui->IO.ImeWindowHandle) + if (HIMC himc = ImmGetContext(hwnd)) + { + COMPOSITIONFORM cf; + cf.ptCurrentPos.x = x; + cf.ptCurrentPos.y = y; + cf.dwStyle = CFS_FORCE_POSITION; + ImmSetCompositionWindow(himc, &cf); + } +} + +#else + +static void ImeSetInputScreenPosFn_DefaultImpl(int, int) {} + +#endif + +//----------------------------------------------------------------------------- +// HELP +//----------------------------------------------------------------------------- + +void ImGui::ShowMetricsWindow(bool* p_open) +{ + if (ImGui::Begin("ImGui Metrics", p_open)) + { + ImGui::Text("Dear ImGui %s", ImGui::GetVersion()); + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + ImGui::Text("%d vertices, %d indices (%d triangles)", ImGui::GetIO().MetricsRenderVertices, ImGui::GetIO().MetricsRenderIndices, ImGui::GetIO().MetricsRenderIndices / 3); + ImGui::Text("%d allocations", (int)GImAllocatorActiveAllocationsCount); + static bool show_clip_rects = true; + ImGui::Checkbox("Show clipping rectangles when hovering draw commands", &show_clip_rects); + ImGui::Separator(); + + struct Funcs + { + static void NodeDrawList(ImGuiWindow* window, ImDrawList* draw_list, const char* label) + { + bool node_open = ImGui::TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, draw_list->CmdBuffer.Size); + if (draw_list == ImGui::GetWindowDrawList()) + { + ImGui::SameLine(); + ImGui::TextColored(ImColor(255,100,100), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) + if (node_open) ImGui::TreePop(); + return; + } + + ImDrawList* overlay_draw_list = ImGui::GetOverlayDrawList(); // Render additional visuals into the top-most draw list + if (window && ImGui::IsItemHovered()) + overlay_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!node_open) + return; + + int elem_offset = 0; + for (const ImDrawCmd* pcmd = draw_list->CmdBuffer.begin(); pcmd < draw_list->CmdBuffer.end(); elem_offset += pcmd->ElemCount, pcmd++) + { + if (pcmd->UserCallback == NULL && pcmd->ElemCount == 0) + continue; + if (pcmd->UserCallback) + { + ImGui::BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); + continue; + } + ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; + bool pcmd_node_open = ImGui::TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "Draw %4d %s vtx, tex 0x%p, clip_rect (%4.0f,%4.0f)-(%4.0f,%4.0f)", pcmd->ElemCount, draw_list->IdxBuffer.Size > 0 ? "indexed" : "non-indexed", pcmd->TextureId, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); + if (show_clip_rects && ImGui::IsItemHovered()) + { + ImRect clip_rect = pcmd->ClipRect; + ImRect vtxs_rect; + for (int i = elem_offset; i < elem_offset + (int)pcmd->ElemCount; i++) + vtxs_rect.Add(draw_list->VtxBuffer[idx_buffer ? idx_buffer[i] : i].pos); + clip_rect.Floor(); overlay_draw_list->AddRect(clip_rect.Min, clip_rect.Max, IM_COL32(255,255,0,255)); + vtxs_rect.Floor(); overlay_draw_list->AddRect(vtxs_rect.Min, vtxs_rect.Max, IM_COL32(255,0,255,255)); + } + if (!pcmd_node_open) + continue; + + // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. + ImGuiListClipper clipper(pcmd->ElemCount/3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. + while (clipper.Step()) + for (int prim = clipper.DisplayStart, vtx_i = elem_offset + clipper.DisplayStart*3; prim < clipper.DisplayEnd; prim++) + { + char buf[300]; + char *buf_p = buf, *buf_end = buf + IM_ARRAYSIZE(buf); + ImVec2 triangles_pos[3]; + for (int n = 0; n < 3; n++, vtx_i++) + { + ImDrawVert& v = draw_list->VtxBuffer[idx_buffer ? idx_buffer[vtx_i] : vtx_i]; + triangles_pos[n] = v.pos; + buf_p += ImFormatString(buf_p, (int)(buf_end - buf_p), "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", (n == 0) ? "vtx" : " ", vtx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); + } + ImGui::Selectable(buf, false); + if (ImGui::IsItemHovered()) + { + ImDrawListFlags backup_flags = overlay_draw_list->Flags; + overlay_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines at is more readable for very large and thin triangles. + overlay_draw_list->AddPolyline(triangles_pos, 3, IM_COL32(255,255,0,255), true, 1.0f); + overlay_draw_list->Flags = backup_flags; + } + } + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + static void NodeWindows(ImVector& windows, const char* label) + { + if (!ImGui::TreeNode(label, "%s (%d)", label, windows.Size)) + return; + for (int i = 0; i < windows.Size; i++) + Funcs::NodeWindow(windows[i], "Window"); + ImGui::TreePop(); + } + + static void NodeWindow(ImGuiWindow* window, const char* label) + { + if (!ImGui::TreeNode(window, "%s '%s', %d @ 0x%p", label, window->Name, window->Active || window->WasActive, window)) + return; + ImGuiWindowFlags flags = window->Flags; + NodeDrawList(window, window->DrawList, "DrawList"); + ImGui::BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), SizeContents (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->SizeContents.x, window->SizeContents.y); + ImGui::BulletText("Flags: 0x%08X (%s%s%s%s%s%s..)", flags, + (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", + (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : ""); + ImGui::BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f)", window->Scroll.x, GetScrollMaxX(window), window->Scroll.y, GetScrollMaxY(window)); + ImGui::BulletText("Active: %d, WriteAccessed: %d", window->Active, window->WriteAccessed); + ImGui::BulletText("NavLastIds: 0x%08X,0x%08X, NavLayerActiveMask: %X", window->NavLastIds[0], window->NavLastIds[1], window->DC.NavLayerActiveMask); + ImGui::BulletText("NavLastChildNavWindow: %s", window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); + if (window->NavRectRel[0].IsFinite()) + ImGui::BulletText("NavRectRel[0]: (%.1f,%.1f)(%.1f,%.1f)", window->NavRectRel[0].Min.x, window->NavRectRel[0].Min.y, window->NavRectRel[0].Max.x, window->NavRectRel[0].Max.y); + else + ImGui::BulletText("NavRectRel[0]: "); + if (window->RootWindow != window) NodeWindow(window->RootWindow, "RootWindow"); + if (window->DC.ChildWindows.Size > 0) NodeWindows(window->DC.ChildWindows, "ChildWindows"); + ImGui::BulletText("Storage: %d bytes", window->StateStorage.Data.Size * (int)sizeof(ImGuiStorage::Pair)); + ImGui::TreePop(); + } + }; + + // Access private state, we are going to display the draw lists from last frame + ImGuiContext& g = *GImGui; + Funcs::NodeWindows(g.Windows, "Windows"); + if (ImGui::TreeNode("DrawList", "Active DrawLists (%d)", g.DrawDataBuilder.Layers[0].Size)) + { + for (int i = 0; i < g.DrawDataBuilder.Layers[0].Size; i++) + Funcs::NodeDrawList(NULL, g.DrawDataBuilder.Layers[0][i], "DrawList"); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Popups", "Open Popups Stack (%d)", g.OpenPopupStack.Size)) + { + for (int i = 0; i < g.OpenPopupStack.Size; i++) + { + ImGuiWindow* window = g.OpenPopupStack[i].Window; + ImGui::BulletText("PopupID: %08x, Window: '%s'%s%s", g.OpenPopupStack[i].PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? " ChildWindow" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? " ChildMenu" : ""); + } + ImGui::TreePop(); + } + if (ImGui::TreeNode("Internal state")) + { + const char* input_source_names[] = { "None", "Mouse", "Nav", "NavGamepad", "NavKeyboard" }; IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_Count_); + ImGui::Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); + ImGui::Text("HoveredRootWindow: '%s'", g.HoveredRootWindow ? g.HoveredRootWindow->Name : "NULL"); + ImGui::Text("HoveredId: 0x%08X/0x%08X (%.2f sec)", g.HoveredId, g.HoveredIdPreviousFrame, g.HoveredIdTimer); // Data is "in-flight" so depending on when the Metrics window is called we may see current frame information or not + ImGui::Text("ActiveId: 0x%08X/0x%08X (%.2f sec), ActiveIdSource: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, input_source_names[g.ActiveIdSource]); + ImGui::Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); + ImGui::Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); + ImGui::Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); + ImGui::Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); + ImGui::Text("NavActivateId: 0x%08X, NavInputId: 0x%08X", g.NavActivateId, g.NavInputId); + ImGui::Text("NavDisableHighlight: %d, NavDisableMouseHover: %d", g.NavDisableHighlight, g.NavDisableMouseHover); + ImGui::Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); + ImGui::TreePop(); + } + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- + +// Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed. +// Prefer just including imgui_internal.h from your code rather than using this define. If a declaration is missing from imgui_internal.h add it or request it on the github. +#ifdef IMGUI_INCLUDE_IMGUI_USER_INL +#include "imgui_user.inl" +#endif + +//----------------------------------------------------------------------------- diff --git a/attachments/simple_engine/imgui/imgui.h b/attachments/simple_engine/imgui/imgui.h new file mode 100644 index 00000000..bd63a2b0 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui.h @@ -0,0 +1,1787 @@ +// dear imgui, v1.60 WIP +// (headers) + +// See imgui.cpp file for documentation. +// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. +// Read 'Programmer guide' in imgui.cpp for notes on how to setup ImGui in your codebase. +// Get latest version at https://github.com/ocornut/imgui + +#pragma once + +// User-editable configuration files (edit stock imconfig.h or define IMGUI_USER_CONFIG to your own filename) +#ifdef IMGUI_USER_CONFIG +#include IMGUI_USER_CONFIG +#endif +#if !defined(IMGUI_DISABLE_INCLUDE_IMCONFIG_H) || defined(IMGUI_INCLUDE_IMCONFIG_H) +#include "imconfig.h" +#endif + +#include // FLT_MAX +#include // va_list +#include // ptrdiff_t, NULL +#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp + +#define IMGUI_VERSION "1.60 WIP" + +// Define attributes of all API symbols declarations, e.g. for DLL under Windows. +#ifndef IMGUI_API +#define IMGUI_API +#endif + +// Define assertion handler. +#ifndef IM_ASSERT +#include +#define IM_ASSERT(_EXPR) assert(_EXPR) +#endif + +// Helpers +// Some compilers support applying printf-style warnings to user functions. +#if defined(__clang__) || defined(__GNUC__) +#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) +#else +#define IM_FMTARGS(FMT) +#define IM_FMTLIST(FMT) +#endif +#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR)/sizeof(*_ARR))) +#define IM_OFFSETOF(_TYPE,_MEMBER) ((size_t)&(((_TYPE*)0)->_MEMBER)) // Offset of _MEMBER within _TYPE. Standardized as offsetof() in modern C++. + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" +#endif + +// Forward declarations +struct ImDrawChannel; // Temporary storage for outputting drawing commands out of order, used by ImDrawList::ChannelsSplit() +struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call) +struct ImDrawData; // All draw command lists required to render the frame +struct ImDrawList; // A single draw command list (generally one per window) +struct ImDrawListSharedData; // Data shared among multiple draw lists (typically owned by parent ImGui context, but you may create one yourself) +struct ImDrawVert; // A single vertex (20 bytes by default, override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) +struct ImFont; // Runtime data for a single font within a parent ImFontAtlas +struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader +struct ImFontConfig; // Configuration data when adding a font or merging fonts +struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 +struct ImGuiIO; // Main configuration and I/O between your application and ImGui +struct ImGuiOnceUponAFrame; // Simple helper for running a block of code not more than once a frame, used by IMGUI_ONCE_UPON_A_FRAME macro +struct ImGuiStorage; // Simple custom key value storage +struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTextFilter; // Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +struct ImGuiTextBuffer; // Text buffer for logging/accumulating text +struct ImGuiTextEditCallbackData; // Shared state of ImGui::InputText() when using custom ImGuiTextEditCallback (rare/advanced use) +struct ImGuiSizeCallbackData; // Structure used to constraint window size in custom ways when using custom ImGuiSizeCallback (rare/advanced use) +struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiContext; // ImGui context (opaque) + +// Typedefs and Enumerations (declared as int for compatibility and to not pollute the top of this file) +typedef unsigned int ImU32; // 32-bit unsigned integer (typically used to store packed colors) +typedef unsigned int ImGuiID; // unique ID used by widgets (typically hashed from a stack of string) +typedef unsigned short ImWchar; // character for keyboard input/display +typedef void* ImTextureID; // user data to identify a texture (this is whatever to you want it to be! read the FAQ about ImTextureID in imgui.cpp) +typedef int ImGuiCol; // enum: a color identifier for styling // enum ImGuiCol_ +typedef int ImGuiCond; // enum: a condition for Set*() // enum ImGuiCond_ +typedef int ImGuiKey; // enum: a key identifier (ImGui-side enum) // enum ImGuiKey_ +typedef int ImGuiNavInput; // enum: an input identifier for navigation // enum ImGuiNavInput_ +typedef int ImGuiMouseCursor; // enum: a mouse cursor identifier // enum ImGuiMouseCursor_ +typedef int ImGuiStyleVar; // enum: a variable identifier for styling // enum ImGuiStyleVar_ +typedef int ImDrawCornerFlags; // flags: for ImDrawList::AddRect*() etc. // enum ImDrawCornerFlags_ +typedef int ImDrawListFlags; // flags: for ImDrawList // enum ImDrawListFlags_ +typedef int ImFontAtlasFlags; // flags: for ImFontAtlas // enum ImFontAtlasFlags_ +typedef int ImGuiColorEditFlags; // flags: for ColorEdit*(), ColorPicker*() // enum ImGuiColorEditFlags_ +typedef int ImGuiColumnsFlags; // flags: for *Columns*() // enum ImGuiColumnsFlags_ +typedef int ImGuiDragDropFlags; // flags: for *DragDrop*() // enum ImGuiDragDropFlags_ +typedef int ImGuiComboFlags; // flags: for BeginCombo() // enum ImGuiComboFlags_ +typedef int ImGuiFocusedFlags; // flags: for IsWindowFocused() // enum ImGuiFocusedFlags_ +typedef int ImGuiHoveredFlags; // flags: for IsItemHovered() etc. // enum ImGuiHoveredFlags_ +typedef int ImGuiInputTextFlags; // flags: for InputText*() // enum ImGuiInputTextFlags_ +typedef int ImGuiNavFlags; // flags: for io.NavFlags // enum ImGuiNavFlags_ +typedef int ImGuiSelectableFlags; // flags: for Selectable() // enum ImGuiSelectableFlags_ +typedef int ImGuiTreeNodeFlags; // flags: for TreeNode*(),CollapsingHeader()// enum ImGuiTreeNodeFlags_ +typedef int ImGuiWindowFlags; // flags: for Begin*() // enum ImGuiWindowFlags_ +typedef int (*ImGuiTextEditCallback)(ImGuiTextEditCallbackData *data); +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); +#if defined(_MSC_VER) && !defined(__clang__) +typedef unsigned __int64 ImU64; // 64-bit unsigned integer +#else +typedef unsigned long long ImU64; // 64-bit unsigned integer +#endif + +// Others helpers at bottom of the file: +// class ImVector<> // Lightweight std::vector like class. +// IMGUI_ONCE_UPON_A_FRAME // Execute a block of code once per frame only (convenient for creating UI within deep-nested code that runs multiple times) + +struct ImVec2 +{ + float x, y; + ImVec2() { x = y = 0.0f; } + ImVec2(float _x, float _y) { x = _x; y = _y; } + float operator[] (size_t idx) const { IM_ASSERT(idx == 0 || idx == 1); return (&x)[idx]; } // We very rarely use this [] operator, thus an assert is fine. +#ifdef IM_VEC2_CLASS_EXTRA // Define constructor and implicit cast operators in imconfig.h to convert back<>forth from your math types and ImVec2. + IM_VEC2_CLASS_EXTRA +#endif +}; + +struct ImVec4 +{ + float x, y, z, w; + ImVec4() { x = y = z = w = 0.0f; } + ImVec4(float _x, float _y, float _z, float _w) { x = _x; y = _y; z = _z; w = _w; } +#ifdef IM_VEC4_CLASS_EXTRA // Define constructor and implicit cast operators in imconfig.h to convert back<>forth from your math types and ImVec4. + IM_VEC4_CLASS_EXTRA +#endif +}; + +// ImGui end-user API +// In a namespace so that user can add extra functions in a separate file (e.g. Value() helpers for your vector or common types) +namespace ImGui +{ + // Context creation and access, if you want to use multiple context, share context between modules (e.g. DLL). + // All contexts share a same ImFontAtlas by default. If you want different font atlas, you can new() them and overwrite the GetIO().Fonts variable of an ImGui context. + // All those functions are not reliant on the current context. + IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); + IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = Destroy current context + IMGUI_API ImGuiContext* GetCurrentContext(); + IMGUI_API void SetCurrentContext(ImGuiContext* ctx); + + // Main + IMGUI_API ImGuiIO& GetIO(); + IMGUI_API ImGuiStyle& GetStyle(); + IMGUI_API void NewFrame(); // start a new ImGui frame, you can submit any command from this point until Render()/EndFrame(). + IMGUI_API void Render(); // ends the ImGui frame, finalize the draw data. (Obsolete: optionally call io.RenderDrawListsFn if set. Nowadays, prefer calling your render function yourself.) + IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. (Obsolete: this used to be passed to your io.RenderDrawListsFn() function.) + IMGUI_API void EndFrame(); // ends the ImGui frame. automatically called by Render(), so most likely don't need to ever call that yourself directly. If you don't need to render you may call EndFrame() but you'll have wasted CPU already. If you don't need to render, better to not create any imgui windows instead! + + // Demo, Debug, Informations + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create demo/test window (previously called ShowTestWindow). demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create metrics window. display ImGui internals: draw commands (with individual draw calls and vertices), window list, basic internal state, etc. + IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) + IMGUI_API bool ShowStyleSelector(const char* label); + IMGUI_API void ShowFontSelector(const char* label); + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as a end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); + + // Styles + IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // New, recommended style + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // Classic imgui style (default) + IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // Best used with borders and a custom, thicker font + + // Window + IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // push window to the stack and start appending to it. see .cpp for details. return false when window is collapsed (so you can early out in your code) but you always need to call End() regardless. 'bool* p_open' creates a widget on the upper-right to close the window (which sets your bool to false). + IMGUI_API void End(); // always call even if Begin() return false (which indicates a collapsed window)! finish appending to current window, pop it off the window stack. + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0); // begin a scrolling region. size==0.0f: use remaining window size, size<0.0f: use remaining window size minus abs(size). size>0.0f: fixed size. each axis can use a different mode, e.g. ImVec2(0,400). + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0); // " + IMGUI_API void EndChild(); // always call even if BeginChild() return false (which indicates a collapsed or clipping child window) + IMGUI_API ImVec2 GetContentRegionMax(); // current content boundaries (typically window boundaries including scrolling, or current column boundaries), in windows coordinates + IMGUI_API ImVec2 GetContentRegionAvail(); // == GetContentRegionMax() - GetCursorPos() + IMGUI_API float GetContentRegionAvailWidth(); // + IMGUI_API ImVec2 GetWindowContentRegionMin(); // content boundaries min (roughly (0,0)-Scroll), in window coordinates + IMGUI_API ImVec2 GetWindowContentRegionMax(); // content boundaries max (roughly (0,0)+Size-Scroll) where Size can be override with SetNextWindowContentSize(), in window coordinates + IMGUI_API float GetWindowContentRegionWidth(); // + IMGUI_API ImDrawList* GetWindowDrawList(); // get rendering command-list if you want to append your own draw primitives + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList api) + IMGUI_API ImVec2 GetWindowSize(); // get current window size + IMGUI_API float GetWindowWidth(); + IMGUI_API float GetWindowHeight(); + IMGUI_API bool IsWindowCollapsed(); + IMGUI_API bool IsWindowAppearing(); + IMGUI_API void SetWindowFontScale(float scale); // per-window font scale. Adjust IO.FontGlobalScale if you want to scale all windows + + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0,0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ enforce the range of scrollbars). not including window decorations (title bar, menu bar, etc.). set an axis to 0.0f to leave it automatic. call before Begin() + IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() + IMGUI_API void SetNextWindowFocus(); // set next window to be focused / front-most. call before Begin() + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily modify ImGuiCol_WindowBg/ChildBg/PopupBg. + IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0,0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). + IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / front-most. prefer using SetNextWindowFocus(). + IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. + IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. + IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state + IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / front-most. use NULL to remove focus. + + IMGUI_API float GetScrollX(); // get scrolling amount [0..GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0..GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.X - WindowSize.X + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.Y - WindowSize.Y + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0..GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0..GetScrollMaxY()] + IMGUI_API void SetScrollHere(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollFromPosY(float pos_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position valid. use GetCursorPos() or GetCursorStartPos()+offset to get valid positions. + IMGUI_API void SetStateStorage(ImGuiStorage* tree); // replace tree state storage with our own (if you want to manipulate it yourself, typically clear subsection of it) + IMGUI_API ImGuiStorage* GetStateStorage(); + + // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); + IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); + IMGUI_API void PopStyleColor(int count = 1); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); + IMGUI_API void PopStyleVar(int count = 1); + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwhise use GetColorU32() to get style color + style alpha. + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied + IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied + + // Parameters stacks (current window) + IMGUI_API void PushItemWidth(float item_width); // width of items for the common item+label case, pixels. 0.0f = default to ~2/3 of windows width, >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -1.0f always align width to the right side) + IMGUI_API void PopItemWidth(); + IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position + IMGUI_API void PushTextWrapPos(float wrap_pos_x = 0.0f); // word-wrapping for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PopTextWrapPos(); + IMGUI_API void PushAllowKeyboardFocus(bool allow_keyboard_focus); // allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets + IMGUI_API void PopAllowKeyboardFocus(); + IMGUI_API void PushButtonRepeat(bool repeat); // in 'repeat' mode, Button*() functions return repeated true in a typematic manner (using io.KeyRepeatDelay/io.KeyRepeatRate setting). Note that you can call IsItemActive() after any Button() to tell if the button is held in the current frame. + IMGUI_API void PopButtonRepeat(); + + // Cursor / Layout + IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. + IMGUI_API void SameLine(float pos_x = 0.0f, float spacing_w = -1.0f); // call between widgets or groups to layout them horizontally + IMGUI_API void NewLine(); // undo a SameLine() + IMGUI_API void Spacing(); // add vertical spacing + IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by style.IndentSpacing or indent_w if != 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by style.IndentSpacing or indent_w if != 0 + IMGUI_API void BeginGroup(); // lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) + IMGUI_API void EndGroup(); + IMGUI_API ImVec2 GetCursorPos(); // cursor position is relative to window position + IMGUI_API float GetCursorPosX(); // " + IMGUI_API float GetCursorPosY(); // " + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // " + IMGUI_API void SetCursorPosX(float x); // " + IMGUI_API void SetCursorPosY(float y); // " + IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute screen coordinates [0..io.DisplaySize] (useful to work with ImDrawList API) + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute screen coordinates [0..io.DisplaySize] + IMGUI_API void AlignTextToFramePadding(); // vertically align/lower upcoming text to FramePadding.y so that it will aligns to upcoming widgets (call if you have text on a line before regular widgets) + IMGUI_API float GetTextLineHeight(); // ~ FontSize + IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) + IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 + IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) + + // Columns + // You can also use SameLine(pos_x) for simplified columns. The columns API is still work-in-progress and rather lacking. + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool border = true); + IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished + IMGUI_API int GetColumnIndex(); // get current column index + IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column + IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column + IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f + IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column + IMGUI_API int GetColumnsCount(); + + // ID scopes + // If you are creating widgets in a loop you most likely want to push a unique identifier (e.g. object pointer, loop index) so ImGui can differentiate them. + // You can also use the "##foobar" syntax within widget label to distinguish them from each others. Read "A primer on the use of labels/IDs" in the FAQ for more details. + IMGUI_API void PushID(const char* str_id); // push identifier into the ID stack. IDs are hash of the entire stack! + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); + IMGUI_API void PushID(const void* ptr_id); + IMGUI_API void PushID(int int_id); + IMGUI_API void PopID(); + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + + // Widgets: Text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // simple formatted text + IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void TextDisabled(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextDisabledV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextWrapped(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). + IMGUI_API void TextWrappedV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void LabelText(const char* label, const char* fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets + IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() + IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void Bullet(); // draw a small circle and keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + + // Widgets: Main + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0,0)); // button + IMGUI_API bool SmallButton(const char* label); // button with FramePadding=(0,0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size); // button behavior without the visuals, useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0,0), const ImVec2& uv1 = ImVec2(1,1), const ImVec4& tint_col = ImVec4(1,1,1,1), const ImVec4& border_col = ImVec4(0,0,0,0)); + IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0,0), const ImVec2& uv1 = ImVec2(1,1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0,0,0,0), const ImVec4& tint_col = ImVec4(1,1,1,1)); // <0 frame_padding uses default frame padding settings. 0 for no padding + IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); + IMGUI_API bool RadioButton(const char* label, bool active); + IMGUI_API bool RadioButton(const char* label, int* v, int v_button); + IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0), int stride = sizeof(float)); + IMGUI_API void PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0)); + IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0), int stride = sizeof(float)); + IMGUI_API void PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0)); + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-1,0), const char* overlay = NULL); + + // Widgets: Combo Box + // The new BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it. + // The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! + IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" + IMGUI_API bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items = -1); + + // Widgets: Drags (tip: ctrl+click on a drag box to input with keyboard. manually input values aren't clamped, can go off-bounds) + // For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every functions, note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", const char* display_format_max = NULL, float power = 1.0f); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f", const char* display_format_max = NULL); + + // Widgets: Input with Keyboard + IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0,0), ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputFloat2(const char* label, float v[2], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputFloat3(const char* label, float v[3], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputFloat4(const char* label, float v[4], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags extra_flags = 0); + + // Widgets: Sliders (tip: ctrl+click on a slider to input with keyboard. manually input values aren't clamped, can go off-bounds) + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); // adjust display_format to decorate the value with a prefix or a suffix for in-slider labels or unit display. Use power!=1.0 for logarithmic sliders + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* display_format = "%.0f"); + + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little colored preview square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // Note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can the pass the address of a first float element out of a contiguous structure, e.g. &myvector.x + IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0)); // display a colored square/button, hover for details, return true when pressed. + IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. + + // Widgets: Trees + IMGUI_API bool TreeNode(const char* label); // if returning 'true' the node is open and the tree id is pushed into the id stack. user is responsible for calling TreePop(). + IMGUI_API bool TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(2); // read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). + IMGUI_API bool TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(2); // " + IMGUI_API bool TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + IMGUI_API bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call Push/Pop yourself for layout purpose + IMGUI_API void TreePush(const void* ptr_id = NULL); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopId() + IMGUI_API void TreeAdvanceToLabelPos(); // advance cursor x position by GetTreeNodeToLabelSpacing() + IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode + IMGUI_API void SetNextTreeNodeOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). + IMGUI_API bool CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags = 0); // when 'p_open' isn't NULL, display an additional small close button on upper right of the header + + // Widgets: Selectable / Lists + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1); + IMGUI_API bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0,0)); // use if you want to reimplement ListBox() will custom data or interactions. make sure to call ListBoxFooter() afterwards. + IMGUI_API bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1); // " + IMGUI_API void ListBoxFooter(); // terminate the scrolling region + + // Widgets: Value() Helpers. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) + IMGUI_API void Value(const char* prefix, bool b); + IMGUI_API void Value(const char* prefix, int v); + IMGUI_API void Value(const char* prefix, unsigned int v); + IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); + + // Tooltips + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set text tooltip under mouse-cursor, typically use with ImGui::IsItemHovered(). overidde any previous call to SetTooltip(). + IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void BeginTooltip(); // begin/append a tooltip window. to create full-featured tooltip (with any kind of contents). + IMGUI_API void EndTooltip(); + + // Menus + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). + IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! + IMGUI_API void EndMenu(); // only call EndBegin() if BeginMenu() returns true! + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. shortcuts are displayed for convenience but not processed by ImGui at the moment + IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL + + // Popups + IMGUI_API void OpenPopup(const char* str_id); // call to mark popup as open (don't call every frame!). popups are closed when user click outside, or if CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. By default, Selectable()/MenuItem() are calling CloseCurrentPopup(). Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. only call EndPopup() if BeginPopup() returns true! + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked on last item. if you can pass a NULL str_id only if the previous item had an id. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, int mouse_button = 1, bool also_over_items = true); // helper to open and begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked in void (where there are no imgui windows). + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // modal dialog (regular window with title bar, block interactions behind the modal window, can't close the modal window by clicking outside) + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + IMGUI_API bool OpenPopupOnItemClick(const char* str_id = NULL, int mouse_button = 1); // helper to open popup when clicked on last item. return true when just opened. + IMGUI_API bool IsPopupOpen(const char* str_id); // return true if the popup is open + IMGUI_API void CloseCurrentPopup(); // close the popup we have begin-ed into. clicking on a MenuItem or Selectable automatically close the current popup. + + // Logging/Capture: all text output from interface is captured to tty/file/clipboard. By default, tree nodes are automatically opened during logging. + IMGUI_API void LogToTTY(int max_depth = -1); // start logging to tty + IMGUI_API void LogToFile(int max_depth = -1, const char* filename = NULL); // start logging to file + IMGUI_API void LogToClipboard(int max_depth = -1); // start logging to OS clipboard + IMGUI_API void LogFinish(); // stop logging (close file, etc.) + IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard + IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + + // Drag and Drop + // [BETA API] Missing Demo code. API may evolve. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call when the current item is active. If this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t size, ImGuiCond cond = 0);// type is a user defined string of maximum 12 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. + IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! + IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive an item. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() + IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. + IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! + + // Clipping + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + IMGUI_API void PopClipRect(); + + // Focus, Activation + // (Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHere()" when applicable, to make your code more forward compatible when navigation branch is merged) + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. Please use instead of "if (IsWindowAppearing()) SetScrollHere()" to signify "default item". + IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + + // Utilities + IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. + IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited- items that don't interact will always return false) + IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? + IMGUI_API bool IsItemClicked(int mouse_button = 0); // is the last item clicked? (e.g. button/node just clicked on) + IMGUI_API bool IsItemVisible(); // is the last item visible? (aka not out of sight due to clipping/scrolling.) + IMGUI_API bool IsAnyItemHovered(); + IMGUI_API bool IsAnyItemActive(); + IMGUI_API bool IsAnyItemFocused(); + IMGUI_API ImVec2 GetItemRectMin(); // get bounding rectangle of last item, in screen space + IMGUI_API ImVec2 GetItemRectMax(); // " + IMGUI_API ImVec2 GetItemRectSize(); // get size of last item, in screen space + IMGUI_API void SetItemAllowOverlap(); // allow last item to be overlapped by a subsequent item. sometimes useful with invisible buttons, selectables, etc. to catch unused area. + IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags = 0); // is current window focused? or its root/child, depending on flags. see flags for options. + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags = 0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. + IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. + IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. + IMGUI_API float GetTime(); + IMGUI_API int GetFrameCount(); + IMGUI_API ImDrawList* GetOverlayDrawList(); // this draw list will be the last rendered one, useful to quickly draw overlays shapes/text + IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); + IMGUI_API const char* GetStyleColorName(ImGuiCol idx); + IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // calculate coarse clipping for large list of evenly sized items. Prefer using the ImGuiListClipper higher-level helper if you can. + + IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame + IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) + + IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); + IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4& in); + IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); + IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); + + // Inputs + IMGUI_API int GetKeyIndex(ImGuiKey imgui_key); // map ImGuiKey_* values into user's key index. == io.KeyMap[key] + IMGUI_API bool IsKeyDown(int user_key_index); // is key being held. == io.KeysDown[user_key_index]. note that imgui doesn't know the semantic of each entry of io.KeyDown[]. Use your own indices/enums according to how your backend/engine stored them into KeyDown[]! + IMGUI_API bool IsKeyPressed(int user_key_index, bool repeat = true); // was key pressed (went from !Down to Down). if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(int user_key_index); // was key released (went from Down to !Down).. + IMGUI_API int GetKeyPressedAmount(int key_index, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API bool IsMouseDown(int button); // is mouse button held + IMGUI_API bool IsAnyMouseDown(); // is any mouse button held + IMGUI_API bool IsMouseClicked(int button, bool repeat = false); // did mouse button clicked (went from !Down to Down) + IMGUI_API bool IsMouseDoubleClicked(int button); // did mouse button double-clicked. a double-click returns false in IsMouseClicked(). uses io.MouseDoubleClickTime. + IMGUI_API bool IsMouseReleased(int button); // did mouse button released (went from Down to !Down) + IMGUI_API bool IsMouseDragging(int button = 0, float lock_threshold = -1.0f); // is mouse dragging. if lock_threshold < -1.0f uses io.MouseDraggingThreshold + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true); // is mouse hovering given bounding rect (in screen space). clipped by current clipping settings. disregarding of consideration of focus/window ordering/blocked by a popup. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve backup of mouse positioning at the time of opening popup we have BeginPopup() into + IMGUI_API ImVec2 GetMouseDragDelta(int button = 0, float lock_threshold = -1.0f); // dragging amount since clicking. if lock_threshold < -1.0f uses io.MouseDraggingThreshold + IMGUI_API void ResetMouseDragDelta(int button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired cursor type, reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor type); // set desired cursor type + IMGUI_API void CaptureKeyboardFromApp(bool capture = true); // manually override io.WantCaptureKeyboard flag next frame (said flag is entirely left for your application handle). e.g. force capture keyboard when your widget is being hovered. + IMGUI_API void CaptureMouseFromApp(bool capture = true); // manually override io.WantCaptureMouse flag next frame (said flag is entirely left for your application handle). + + // Clipboard Utilities (also see the LogToClipboard() function to capture or output text data to the clipboard) + IMGUI_API const char* GetClipboardText(); + IMGUI_API void SetClipboardText(const char* text); + + // Memory Utilities + // All those functions are not reliant on the current context. + // If you reload the contents of imgui.cpp at runtime, you may need to call SetCurrentContext() + SetAllocatorFunctions() again. + IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void(*free_func)(void* ptr, void* user_data), void* user_data = NULL); + IMGUI_API void* MemAlloc(size_t size); + IMGUI_API void MemFree(void* ptr); + +} // namespace ImGui + +// Flags for ImGui::Begin() +enum ImGuiWindowFlags_ +{ + ImGuiWindowFlags_NoTitleBar = 1 << 0, // Disable title-bar + ImGuiWindowFlags_NoResize = 1 << 1, // Disable user resizing with the lower-right grip + ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window + ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programatically) + ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. + ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it + ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame + //ImGuiWindowFlags_ShowBorders = 1 << 7, // Show borders around windows and items (OBSOLETE! Use e.g. style.FrameBorderSize=1.0f to enable borders). + ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file + ImGuiWindowFlags_NoInputs = 1 << 9, // Disable catching mouse or keyboard inputs, hovering test with pass through. + ImGuiWindowFlags_MenuBar = 1 << 10, // Has a menu-bar + ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, // Allow horizontal scrollbar to appear (off by default). You may use SetNextWindowContentSize(ImVec2(width,0.0f)); prior to calling Begin() to specify width. Read code in imgui_demo in the "Horizontal Scrolling" section. + ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, // Disable taking focus when transitioning from hidden to visible state + ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programatically giving it focus) + ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) + ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 16, // Ensure child windows without border uses style.WindowPadding (ignored by default for non-bordered child windows, because more convenient) + ImGuiWindowFlags_ResizeFromAnySide = 1 << 17, // (WIP) Enable resize from any corners and borders. Your back-end needs to honor the different values of io.MouseCursor set by imgui. + ImGuiWindowFlags_NoNavInputs = 1 << 18, // No gamepad/keyboard navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 19, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + + // [Internal] + ImGuiWindowFlags_NavFlattened = 1 << 23, // (WIP) Allow gamepad/keyboard navigation to cross over parent border to this child (only use on child that have no scrolling!) + ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() + ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() + ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() + ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() + ImGuiWindowFlags_ChildMenu = 1 << 28 // Don't use! For internal use by BeginMenu() +}; + +// Flags for ImGui::InputText() +enum ImGuiInputTextFlags_ +{ + ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ + ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef + ImGuiInputTextFlags_CharsUppercase = 1 << 2, // Turn a..z into A..Z + ImGuiInputTextFlags_CharsNoBlank = 1 << 3, // Filter out spaces, tabs + ImGuiInputTextFlags_AutoSelectAll = 1 << 4, // Select entire text when first taking mouse focus + ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, // Return 'true' when Enter is pressed (as opposed to when the value was modified) + ImGuiInputTextFlags_CallbackCompletion = 1 << 6, // Call user function on pressing TAB (for completion handling) + ImGuiInputTextFlags_CallbackHistory = 1 << 7, // Call user function on pressing Up/Down arrows (for history handling) + ImGuiInputTextFlags_CallbackAlways = 1 << 8, // Call user function every time. User code may query cursor position, modify text buffer. + ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, // Call user function to filter character. Modify data->EventChar to replace/filter input, or return 1 to discard character. + ImGuiInputTextFlags_AllowTabInput = 1 << 10, // Pressing TAB input a '\t' character into the text field + ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, // In multi-line mode, unfocus with Enter, add new line with Ctrl+Enter (default is opposite: unfocus with Ctrl+Enter, add line with Enter). + ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, // Disable following the cursor horizontally + ImGuiInputTextFlags_AlwaysInsertMode = 1 << 13, // Insert mode + ImGuiInputTextFlags_ReadOnly = 1 << 14, // Read-only mode + ImGuiInputTextFlags_Password = 1 << 15, // Password mode, display all characters as '*' + ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). + // [Internal] + ImGuiInputTextFlags_Multiline = 1 << 20 // For internal use by InputTextMultiline() +}; + +// Flags for ImGui::TreeNodeEx(), ImGui::CollapsingHeader*() +enum ImGuiTreeNodeFlags_ +{ + ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected + ImGuiTreeNodeFlags_Framed = 1 << 1, // Full colored frame (e.g. for CollapsingHeader) + ImGuiTreeNodeFlags_AllowItemOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack + ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) + ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open + ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Need double-click to open node + ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Only open when clicking on the arrow part. If ImGuiTreeNodeFlags_OpenOnDoubleClick is also set, single-click arrow or double-click all box to open. + ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow + ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). + //ImGuITreeNodeFlags_SpanAllAvailWidth = 1 << 11, // FIXME: TODO: Extend hit box horizontally even if not framed + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 12, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 13, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoAutoOpenOnLog + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiTreeNodeFlags_AllowOverlapMode = ImGuiTreeNodeFlags_AllowItemOverlap +#endif +}; + +// Flags for ImGui::Selectable() +enum ImGuiSelectableFlags_ +{ + ImGuiSelectableFlags_DontClosePopups = 1 << 0, // Clicking this don't close parent popup window + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Selectable frame can span all columns (text will still fit in current column) + ImGuiSelectableFlags_AllowDoubleClick = 1 << 2 // Generate press events on double clicks too +}; + +// Flags for ImGui::BeginCombo() +enum ImGuiComboFlags_ +{ + ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default + ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible. Tip: If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() + ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) + ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible + ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest +}; + +// Flags for ImGui::IsWindowFocused() +enum ImGuiFocusedFlags_ +{ + ImGuiFocusedFlags_ChildWindows = 1 << 0, // IsWindowFocused(): Return true if any children of the window is focused + ImGuiFocusedFlags_RootWindow = 1 << 1, // IsWindowFocused(): Test from root window (top most parent of the current hierarchy) + ImGuiFocusedFlags_AnyWindow = 1 << 2, // IsWindowFocused(): Return true if any window is focused + ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows +}; + +// Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() +enum ImGuiHoveredFlags_ +{ + ImGuiHoveredFlags_Default = 0, // Return true if directly over the item/window, not obstructed by another window, not obstructed by an active popup or modal blocking inputs under them. + ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered + ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) + ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered + ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 3, // Return true even if a popup window is normally blocking access to this item/window + //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 4, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 5, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. + ImGuiHoveredFlags_AllowWhenOverlapped = 1 << 6, // Return true even if the position is overlapped by another window + ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, + ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows +}; + +// Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() +enum ImGuiDragDropFlags_ +{ + // BeginDragDropSource() flags + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disable this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return true, to avoid subsequent user code submitting tooltips. This flag disable this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. + ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. + ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. + // AcceptDragDropPayload() flags + ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. + ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect // For peeking ahead and inspecting the payload before delivery. +}; + +// Standard Drag and Drop payload types. You can define you own payload types using 12-characters long strings. Types starting with '_' are defined by Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3] // Standard type for colors, without alpha. User code may use this type. +#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4] // Standard type for colors. User code may use this type. + +// User fill ImGuiIO.KeyMap[] array with indices into the ImGuiIO.KeysDown[512] array +enum ImGuiKey_ +{ + ImGuiKey_Tab, + ImGuiKey_LeftArrow, + ImGuiKey_RightArrow, + ImGuiKey_UpArrow, + ImGuiKey_DownArrow, + ImGuiKey_PageUp, + ImGuiKey_PageDown, + ImGuiKey_Home, + ImGuiKey_End, + ImGuiKey_Insert, + ImGuiKey_Delete, + ImGuiKey_Backspace, + ImGuiKey_Space, + ImGuiKey_Enter, + ImGuiKey_Escape, + ImGuiKey_A, // for text edit CTRL+A: select all + ImGuiKey_C, // for text edit CTRL+C: copy + ImGuiKey_V, // for text edit CTRL+V: paste + ImGuiKey_X, // for text edit CTRL+X: cut + ImGuiKey_Y, // for text edit CTRL+Y: redo + ImGuiKey_Z, // for text edit CTRL+Z: undo + ImGuiKey_COUNT +}; + +// [BETA] Gamepad/Keyboard directional navigation +// Keyboard: Set io.NavFlags |= ImGuiNavFlags_EnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeyDown[] + io.KeyMap[] arrays. +// Gamepad: Set io.NavFlags |= ImGuiNavFlags_EnableGamepad to enable. Fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). +// Read instructions in imgui.cpp for more details. +enum ImGuiNavInput_ +{ + // Gamepad Mapping + ImGuiNavInput_Activate, // activate / open / toggle / tweak value // e.g. Circle (PS4), A (Xbox), B (Switch), Space (Keyboard) + ImGuiNavInput_Cancel, // cancel / close / exit // e.g. Cross (PS4), B (Xbox), A (Switch), Escape (Keyboard) + ImGuiNavInput_Input, // text input / on-screen keyboard // e.g. Triang.(PS4), Y (Xbox), X (Switch), Return (Keyboard) + ImGuiNavInput_Menu, // tap: toggle menu / hold: focus, move, resize // e.g. Square (PS4), X (Xbox), Y (Switch), Alt (Keyboard) + ImGuiNavInput_DpadLeft, // move / tweak / resize window (w/ PadMenu) // e.g. D-pad Left/Right/Up/Down (Gamepads), Arrow keys (Keyboard) + ImGuiNavInput_DpadRight, // + ImGuiNavInput_DpadUp, // + ImGuiNavInput_DpadDown, // + ImGuiNavInput_LStickLeft, // scroll / move window (w/ PadMenu) // e.g. Left Analog Stick Left/Right/Up/Down + ImGuiNavInput_LStickRight, // + ImGuiNavInput_LStickUp, // + ImGuiNavInput_LStickDown, // + ImGuiNavInput_FocusPrev, // next window (w/ PadMenu) // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) + ImGuiNavInput_FocusNext, // prev window (w/ PadMenu) // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + ImGuiNavInput_TweakSlow, // slower tweaks // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) + ImGuiNavInput_TweakFast, // faster tweaks // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + + // [Internal] Don't use directly! This is used internally to differentiate keyboard from gamepad inputs for behaviors that require to differentiate them. + // Keyboard behavior that have no corresponding gamepad mapping (e.g. CTRL+TAB) may be directly reading from io.KeyDown[] instead of io.NavInputs[]. + ImGuiNavInput_KeyMenu_, // toggle menu // = io.KeyAlt + ImGuiNavInput_KeyLeft_, // move left // = Arrow keys + ImGuiNavInput_KeyRight_, // move right + ImGuiNavInput_KeyUp_, // move up + ImGuiNavInput_KeyDown_, // move down + ImGuiNavInput_COUNT, + ImGuiNavInput_InternalStart_ = ImGuiNavInput_KeyMenu_ +}; + +// [BETA] Gamepad/Keyboard directional navigation flags, stored in io.NavFlags +enum ImGuiNavFlags_ +{ + ImGuiNavFlags_EnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. NewFrame() will automatically fill io.NavInputs[] based on io.KeyDown[]. + ImGuiNavFlags_EnableGamepad = 1 << 1, // Master gamepad navigation enable flag. This is mostly to instruct your imgui back-end to fill io.NavInputs[]. + ImGuiNavFlags_MoveMouse = 1 << 2, // Request navigation to allow moving the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantMoveMouse=true. If enabled you MUST honor io.WantMoveMouse requests in your binding, otherwise ImGui will react as if the mouse is jumping around back and forth. + ImGuiNavFlags_NoCaptureKeyboard = 1 << 3 // Do not set the io.WantCaptureKeyboard flag with io.NavActive is set. +}; + +// Enumeration for PushStyleColor() / PopStyleColor() +enum ImGuiCol_ +{ + ImGuiCol_Text, + ImGuiCol_TextDisabled, + ImGuiCol_WindowBg, // Background of normal windows + ImGuiCol_ChildBg, // Background of child windows + ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows + ImGuiCol_Border, + ImGuiCol_BorderShadow, + ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input + ImGuiCol_FrameBgHovered, + ImGuiCol_FrameBgActive, + ImGuiCol_TitleBg, + ImGuiCol_TitleBgActive, + ImGuiCol_TitleBgCollapsed, + ImGuiCol_MenuBarBg, + ImGuiCol_ScrollbarBg, + ImGuiCol_ScrollbarGrab, + ImGuiCol_ScrollbarGrabHovered, + ImGuiCol_ScrollbarGrabActive, + ImGuiCol_CheckMark, + ImGuiCol_SliderGrab, + ImGuiCol_SliderGrabActive, + ImGuiCol_Button, + ImGuiCol_ButtonHovered, + ImGuiCol_ButtonActive, + ImGuiCol_Header, + ImGuiCol_HeaderHovered, + ImGuiCol_HeaderActive, + ImGuiCol_Separator, + ImGuiCol_SeparatorHovered, + ImGuiCol_SeparatorActive, + ImGuiCol_ResizeGrip, + ImGuiCol_ResizeGripHovered, + ImGuiCol_ResizeGripActive, + ImGuiCol_CloseButton, + ImGuiCol_CloseButtonHovered, + ImGuiCol_CloseButtonActive, + ImGuiCol_PlotLines, + ImGuiCol_PlotLinesHovered, + ImGuiCol_PlotHistogram, + ImGuiCol_PlotHistogramHovered, + ImGuiCol_TextSelectedBg, + ImGuiCol_ModalWindowDarkening, // darken entire screen when a modal window is active + ImGuiCol_DragDropTarget, + ImGuiCol_NavHighlight, // gamepad/keyboard: current highlighted item + ImGuiCol_NavWindowingHighlight, // gamepad/keyboard: when holding NavMenu to focus/move/resize windows + ImGuiCol_COUNT + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //, ImGuiCol_ComboBg = ImGuiCol_PopupBg // ComboBg has been merged with PopupBg, so a redirect isn't accurate. + , ImGuiCol_ChildWindowBg = ImGuiCol_ChildBg, ImGuiCol_Column = ImGuiCol_Separator, ImGuiCol_ColumnHovered = ImGuiCol_SeparatorHovered, ImGuiCol_ColumnActive = ImGuiCol_SeparatorActive +#endif +}; + +// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. +// NB: the enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. During initialization, feel free to just poke into ImGuiStyle directly. +// NB: if changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +enum ImGuiStyleVar_ +{ + // Enum name ......................// Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_Count_ + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiStyleVar_ChildWindowRounding = ImGuiStyleVar_ChildRounding +#endif +}; + +// Enumeration for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() +enum ImGuiColorEditFlags_ +{ + ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (read 3 components from the input pointer). + ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on colored square. + ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. + ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable colored square preview next to the inputs. (e.g. to show only the inputs) + ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview colored square). + ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. + ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). + ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small colored square preview instead. + // User Options (right-click on widget to change some of them). You can set application defaults using SetColorEditOptions(). The idea is that you probably don't want to override them in most of your calls, let the user choose and/or call SetColorEditOptions() during startup. + ImGuiColorEditFlags_AlphaBar = 1 << 9, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. + ImGuiColorEditFlags_AlphaPreview = 1 << 10, // // ColorEdit, ColorPicker, ColorButton: display preview as a transparent color over a checkerboard, instead of opaque. + ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 11, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half checkerboard, instead of opaque. + ImGuiColorEditFlags_HDR = 1 << 12, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). + ImGuiColorEditFlags_RGB = 1 << 13, // [Inputs] // ColorEdit: choose one among RGB/HSV/HEX. ColorPicker: choose any combination using RGB/HSV/HEX. + ImGuiColorEditFlags_HSV = 1 << 14, // [Inputs] // " + ImGuiColorEditFlags_HEX = 1 << 15, // [Inputs] // " + ImGuiColorEditFlags_Uint8 = 1 << 16, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0..255. + ImGuiColorEditFlags_Float = 1 << 17, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0.0f..1.0f floats instead of 0..255 integers. No round-trip of value via integers. + ImGuiColorEditFlags_PickerHueBar = 1 << 18, // [PickerMode] // ColorPicker: bar for Hue, rectangle for Sat/Value. + ImGuiColorEditFlags_PickerHueWheel = 1 << 19, // [PickerMode] // ColorPicker: wheel for Hue, triangle for Sat/Value. + // Internals/Masks + ImGuiColorEditFlags__InputsMask = ImGuiColorEditFlags_RGB|ImGuiColorEditFlags_HSV|ImGuiColorEditFlags_HEX, + ImGuiColorEditFlags__DataTypeMask = ImGuiColorEditFlags_Uint8|ImGuiColorEditFlags_Float, + ImGuiColorEditFlags__PickerMask = ImGuiColorEditFlags_PickerHueWheel|ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags__OptionsDefault = ImGuiColorEditFlags_Uint8|ImGuiColorEditFlags_RGB|ImGuiColorEditFlags_PickerHueBar // Change application default using SetColorEditOptions() +}; + +// Enumeration for GetMouseCursor() +enum ImGuiMouseCursor_ +{ + ImGuiMouseCursor_None = -1, + ImGuiMouseCursor_Arrow = 0, + ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. + ImGuiMouseCursor_ResizeAll, // Unused + ImGuiMouseCursor_ResizeNS, // When hovering over an horizontal border + ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column + ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window + ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window + ImGuiMouseCursor_Count_ +}; + +// Condition for ImGui::SetWindow***(), SetNextWindow***(), SetNextTreeNode***() functions +// All those functions treat 0 as a shortcut to ImGuiCond_Always. From the point of view of the user use this as an enum (don't combine multiple values into flags). +enum ImGuiCond_ +{ + ImGuiCond_Always = 1 << 0, // Set the variable + ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call with succeed) + ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the window has no saved data (if doesn't exist in the .ini file) + ImGuiCond_Appearing = 1 << 3 // Set the variable if the window is appearing after being hidden/inactive (or the first time) + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiSetCond_Always = ImGuiCond_Always, ImGuiSetCond_Once = ImGuiCond_Once, ImGuiSetCond_FirstUseEver = ImGuiCond_FirstUseEver, ImGuiSetCond_Appearing = ImGuiCond_Appearing +#endif +}; + +// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). +// During the frame, prefer using ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. +struct ImGuiStyle +{ + float Alpha; // Global alpha applies to everything in ImGui. + ImVec2 WindowPadding; // Padding within a window. + float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. + float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constraint individual windows, use SetNextWindowSizeConstraints(). + ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. + float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. + float ChildBorderSize; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + float PopupRounding; // Radius of popup window corners rounding. + float PopupBorderSize; // Thickness of border around popup windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 FramePadding; // Padding within a framed rectangle (used by most widgets). + float FrameRounding; // Radius of frame corners rounding. Set to 0.0f to have rectangular frame (used by most widgets). + float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. + ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. + float ScrollbarSize; // Width of the vertical scrollbar, Height of the horizontal scrollbar. + float ScrollbarRounding; // Radius of grab corners for scrollbar. + float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. + float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f,0.5f) for horizontally+vertically centered. + ImVec2 DisplayWindowPadding; // Window positions are clamped to be visible within the display area by at least this amount. Only covers regular windows. + ImVec2 DisplaySafeAreaPadding; // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + bool AntiAliasedLines; // Enable anti-aliasing on lines/borders. Disable if you are really tight on CPU/GPU. + bool AntiAliasedFill; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + ImVec4 Colors[ImGuiCol_COUNT]; + + IMGUI_API ImGuiStyle(); + IMGUI_API void ScaleAllSizes(float scale_factor); +}; + +// This is where your app communicate with ImGui. Access via ImGui::GetIO(). +// Read 'Programmer guide' section in .cpp file for general usage. +struct ImGuiIO +{ + //------------------------------------------------------------------ + // Settings (fill once) // Default value: + //------------------------------------------------------------------ + + ImVec2 DisplaySize; // // Display size, in pixels. For clamping windows positions. + float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. + ImGuiNavFlags NavFlags; // = 0x00 // See ImGuiNavFlags_. Gamepad/keyboard navigation options. + float IniSavingRate; // = 5.0f // Maximum time between saving positions/sizes to .ini file, in seconds. + const char* IniFilename; // = "imgui.ini" // Path to .ini file. NULL to disable .ini saving. + const char* LogFilename; // = "imgui_log.txt" // Path to .log file (default parameter to ImGui::LogToFile when no file is specified). + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + int KeyMap[ImGuiKey_COUNT]; // // Map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. + float KeyRepeatDelay; // = 0.250f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + void* UserData; // = NULL // Store your own data for retrieval by callbacks. + + ImFontAtlas* Fonts; // // Load and assemble one or more fonts into a single tightly packed texture. Output to Fonts array. + float FontGlobalScale; // = 1.0f // Global scale all fonts + bool FontAllowUserScaling; // = false // Allow user scaling text of individual window with CTRL+Wheel. + ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. + ImVec2 DisplayFramebufferScale; // = (1.0f,1.0f) // For retina display or other situations where window coordinates are different from framebuffer coordinates. User storage only, presently not used by ImGui. + ImVec2 DisplayVisibleMin; // (0.0f,0.0f) // If you use DisplaySize as a virtual space larger than your screen, set DisplayVisibleMin/Max to the visible area. + ImVec2 DisplayVisibleMax; // (0.0f,0.0f) // If the values are the same, we defaults to Min=(0.0f) and Max=DisplaySize + + // Advanced/subtle behaviors + bool OptMacOSXBehaviors; // = defined(__APPLE__) // OS X style: Text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl + bool OptCursorBlink; // = true // Enable blinking cursor, for users who consider it annoying. + + //------------------------------------------------------------------ + // Settings (User Functions) + //------------------------------------------------------------------ + + // Optional: access OS clipboard + // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) + const char* (*GetClipboardTextFn)(void* user_data); + void (*SetClipboardTextFn)(void* user_data, const char* text); + void* ClipboardUserData; + + // Optional: notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME in Windows) + // (default to use native imm32 api on Windows) + void (*ImeSetInputScreenPosFn)(int x, int y); + void* ImeWindowHandle; // (Windows) Set this to your HWND to get automatic IME cursor positioning. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // [OBSOLETE] Rendering function, will be automatically called in Render(). Please call your rendering function yourself now! You can obtain the ImDrawData* by calling ImGui::GetDrawData() after Render(). + // See example applications if you are unsure of how to implement this. + void (*RenderDrawListsFn)(ImDrawData* data); +#endif + + //------------------------------------------------------------------ + // Input - Fill before calling NewFrame() + //------------------------------------------------------------------ + + ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX,-FLT_MAX) if mouse is unavailable (on another screen, etc.) + bool MouseDown[5]; // Mouse buttons: left, right, middle + extras. ImGui itself mostly only uses left button (BeginPopupContext** are using right button). Others buttons allows us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. + float MouseWheel; // Mouse wheel: 1 unit scrolls about 5 lines text. + float MouseWheelH; // Mouse wheel (Horizontal). Most users don't have a mouse with an horizontal wheel, may not be filled by all back-ends. + bool MouseDrawCursor; // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). + bool KeyCtrl; // Keyboard modifier pressed: Control + bool KeyShift; // Keyboard modifier pressed: Shift + bool KeyAlt; // Keyboard modifier pressed: Alt + bool KeySuper; // Keyboard modifier pressed: Cmd/Super/Windows + bool KeysDown[512]; // Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). + ImWchar InputCharacters[16+1]; // List of characters input (translated by user from keypress+keyboard state). Fill using AddInputCharacter() helper. + float NavInputs[ImGuiNavInput_COUNT]; // Gamepad inputs (keyboard keys will be auto-mapped and be written here by ImGui::NewFrame) + + // Functions + IMGUI_API void AddInputCharacter(ImWchar c); // Add new character into InputCharacters[] + IMGUI_API void AddInputCharactersUTF8(const char* utf8_chars); // Add new characters into InputCharacters[] from an UTF-8 string + inline void ClearInputCharacters() { InputCharacters[0] = 0; } // Clear the text input buffer manually + + //------------------------------------------------------------------ + // Output - Retrieve after calling NewFrame() + //------------------------------------------------------------------ + + bool WantCaptureMouse; // When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application. This is set by ImGui when it wants to use your mouse (e.g. unclicked mouse is hovering a window, or a widget is active). + bool WantCaptureKeyboard; // When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application. This is set by ImGui when it wants to use your keyboard inputs. + bool WantTextInput; // Mobile/console: when io.WantTextInput is true, you may display an on-screen keyboard. This is set by ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). + bool WantMoveMouse; // MousePos has been altered, back-end should reposition mouse on next frame. Set only when ImGuiNavFlags_MoveMouse flag is enabled in io.NavFlags. + bool NavActive; // Directional navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. + bool NavVisible; // Directional navigation is visible and allowed (will handle ImGuiKey_NavXXX events). + float Framerate; // Application framerate estimation, in frame per second. Solely for convenience. Rolling average estimation based on IO.DeltaTime over 120 frames + int MetricsRenderVertices; // Vertices output during last call to Render() + int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 + int MetricsActiveWindows; // Number of visible root windows (exclude child windows) + ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. + + //------------------------------------------------------------------ + // [Internal] ImGui will maintain those fields. Forward compatibility not guaranteed! + //------------------------------------------------------------------ + + ImVec2 MousePosPrev; // Previous mouse position temporary storage (nb: not for public use, set to MousePos in NewFrame()) + ImVec2 MouseClickedPos[5]; // Position at time of clicking + float MouseClickedTime[5]; // Time of last click (used to figure out double-click) + bool MouseClicked[5]; // Mouse button went from !Down to Down + bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? + bool MouseReleased[5]; // Mouse button went from Down to !Down + bool MouseDownOwned[5]; // Track if button was clicked inside a window. We don't request mouse capture from the application if click started outside ImGui bounds. + float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) + float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + ImVec2 MouseDragMaxDistanceAbs[5]; // Maximum distance, absolute, on each axis, of how much mouse has traveled from the clicking point + float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point + float KeysDownDuration[512]; // Duration the keyboard key has been down (0.0f == just pressed) + float KeysDownDurationPrev[512]; // Previous duration the key has been down + float NavInputsDownDuration[ImGuiNavInput_COUNT]; + float NavInputsDownDurationPrev[ImGuiNavInput_COUNT]; + + IMGUI_API ImGuiIO(); +}; + +//----------------------------------------------------------------------------- +// Obsolete functions (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +namespace ImGui +{ + // OBSOLETED in 1.60 (from Dec 2017) + static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } + static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } + static inline ImVec2 CalcItemRectClosestPoint(const ImVec2& pos, bool on_edge = false, float outward = 0.f) { (void)on_edge; (void)outward; IM_ASSERT(0); return pos; } + // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + static inline void ShowTestWindow() { return ShowDemoWindow(); } + static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } + static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } + static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } + static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } + // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + bool Begin(const char* name, bool* p_open, const ImVec2& size_on_first_use, float bg_alpha_override = -1.0f, ImGuiWindowFlags flags = 0); // Use SetNextWindowSize(size, ImGuiCond_FirstUseEver) + SetNextWindowBgAlpha() instead. + static inline bool IsRootWindowOrAnyChildHovered() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } + static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } + static inline void SetNextWindowPosCenter(ImGuiCond c=0) { ImGuiIO& io = GetIO(); SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), c, ImVec2(0.5f, 0.5f)); } + // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + static inline bool IsItemHoveredRect() { return IsItemHovered(ImGuiHoveredFlags_RectOnly); } + static inline bool IsPosHoveringAnyWindow(const ImVec2&) { IM_ASSERT(0); return false; } // This was misleading and partly broken. You probably want to use the ImGui::GetIO().WantCaptureMouse flag instead. + static inline bool IsMouseHoveringAnyWindow() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } + static inline bool IsMouseHoveringWindow() { return IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); } + // OBSOLETED IN 1.49 (between Apr 2016 and May 2016) + static inline bool CollapsingHeader(const char* label, const char* str_id, bool framed = true, bool default_open = false) { (void)str_id; (void)framed; ImGuiTreeNodeFlags default_open_flags = 1 << 5; return CollapsingHeader(label, (default_open ? default_open_flags : 0)); } +} +#endif + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +// Lightweight std::vector<> like class to avoid dragging dependencies (also: windows implementation of STL with debug enabled is absurdly slow, so let's bypass it so our code runs fast in debug). +// Our implementation does NOT call C++ constructors/destructors. This is intentional and we do not require it. Do not use this class as a straight std::vector replacement in your code! +template +class ImVector +{ +public: + int Size; + int Capacity; + T* Data; + + typedef T value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + + inline ImVector() { Size = Capacity = 0; Data = NULL; } + inline ~ImVector() { if (Data) ImGui::MemFree(Data); } + + inline bool empty() const { return Size == 0; } + inline int size() const { return Size; } + inline int capacity() const { return Capacity; } + + inline value_type& operator[](int i) { IM_ASSERT(i < Size); return Data[i]; } + inline const value_type& operator[](int i) const { IM_ASSERT(i < Size); return Data[i]; } + + inline void clear() { if (Data) { Size = Capacity = 0; ImGui::MemFree(Data); Data = NULL; } } + inline iterator begin() { return Data; } + inline const_iterator begin() const { return Data; } + inline iterator end() { return Data + Size; } + inline const_iterator end() const { return Data + Size; } + inline value_type& front() { IM_ASSERT(Size > 0); return Data[0]; } + inline const value_type& front() const { IM_ASSERT(Size > 0); return Data[0]; } + inline value_type& back() { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline const value_type& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; value_type* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } + + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity/2) : 8; return new_capacity > sz ? new_capacity : sz; } + + inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } + inline void resize(int new_size, const T& v){ if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) Data[n] = v; Size = new_size; } + inline void reserve(int new_capacity) + { + if (new_capacity <= Capacity) + return; + T* new_data = (value_type*)ImGui::MemAlloc((size_t)new_capacity * sizeof(T)); + if (Data) + memcpy(new_data, Data, (size_t)Size * sizeof(T)); + ImGui::MemFree(Data); + Data = new_data; + Capacity = new_capacity; + } + + // NB: &v cannot be pointing inside the ImVector Data itself! e.g. v.push_back(v[10]) is forbidden. + inline void push_back(const value_type& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); Data[Size++] = v; } + inline void pop_back() { IM_ASSERT(Size > 0); Size--; } + inline void push_front(const value_type& v) { if (Size == 0) push_back(v); else insert(Data, v); } + + inline iterator erase(const_iterator it) { IM_ASSERT(it >= Data && it < Data+Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(value_type)); Size--; return Data + off; } + inline iterator insert(const_iterator it, const value_type& v) { IM_ASSERT(it >= Data && it <= Data+Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(value_type)); Data[off] = v; Size++; return Data + off; } + inline bool contains(const value_type& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } +}; + +// Helper: execute a block of code at maximum once a frame. Convenient if you want to quickly create an UI within deep-nested code that runs multiple times every frame. +// Usage: +// static ImGuiOnceUponAFrame oaf; +// if (oaf) +// ImGui::Text("This will be called only once per frame"); +struct ImGuiOnceUponAFrame +{ + ImGuiOnceUponAFrame() { RefFrame = -1; } + mutable int RefFrame; + operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; } +}; + +// Helper macro for ImGuiOnceUponAFrame. Attention: The macro expands into 2 statement so make sure you don't use it within e.g. an if() statement without curly braces. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // Will obsolete +#define IMGUI_ONCE_UPON_A_FRAME static ImGuiOnceUponAFrame imgui_oaf; if (imgui_oaf) +#endif + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +struct ImGuiTextFilter +{ + struct TextRange + { + const char* b; + const char* e; + + TextRange() { b = e = NULL; } + TextRange(const char* _b, const char* _e) { b = _b; e = _e; } + const char* begin() const { return b; } + const char* end() const { return e; } + bool empty() const { return b == e; } + char front() const { return *b; } + static bool is_blank(char c) { return c == ' ' || c == '\t'; } + void trim_blanks() { while (b < e && is_blank(*b)) b++; while (e > b && is_blank(*(e-1))) e--; } + IMGUI_API void split(char separator, ImVector& out); + }; + + char InputBuf[256]; + ImVector Filters; + int CountGrep; + + IMGUI_API ImGuiTextFilter(const char* default_filter = ""); + IMGUI_API bool Draw(const char* label = "Filter (inc,-exc)", float width = 0.0f); // Helper calling InputText+Build + IMGUI_API bool PassFilter(const char* text, const char* text_end = NULL) const; + IMGUI_API void Build(); + void Clear() { InputBuf[0] = 0; Build(); } + bool IsActive() const { return !Filters.empty(); } +}; + +// Helper: Text buffer for logging/accumulating text +struct ImGuiTextBuffer +{ + ImVector Buf; + + ImGuiTextBuffer() { Buf.push_back(0); } + inline char operator[](int i) { return Buf.Data[i]; } + const char* begin() const { return &Buf.front(); } + const char* end() const { return &Buf.back(); } // Buf is zero-terminated, so end() will point on the zero-terminator + int size() const { return Buf.Size - 1; } + bool empty() { return Buf.Size <= 1; } + void clear() { Buf.clear(); Buf.push_back(0); } + void reserve(int capacity) { Buf.reserve(capacity); } + const char* c_str() const { return Buf.Data; } + IMGUI_API void appendf(const char* fmt, ...) IM_FMTARGS(2); + IMGUI_API void appendfv(const char* fmt, va_list args) IM_FMTLIST(2); +}; + +// Helper: Simple Key->value storage +// Typically you don't have to worry about this since a storage is held within each Window. +// We use it to e.g. store collapse state for a tree (Int 0/1), store color edit options. +// This is optimized for efficient reading (dichotomy into a contiguous buffer), rare writing (typically tied to user interactions) +// You can use it as custom user storage for temporary values. Declare your own storage if, for example: +// - You want to manipulate the open/close state of a particular sub-tree in your interface (tree node uses Int 0/1 to store their state). +// - You want to store custom debug data easily without adding or editing structures in your code (probably not efficient, but convenient) +// Types are NOT stored, so it is up to you to make sure your Key don't collide with different types. +struct ImGuiStorage +{ + struct Pair + { + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + Pair(ImGuiID _key, int _val_i) { key = _key; val_i = _val_i; } + Pair(ImGuiID _key, float _val_f) { key = _key; val_f = _val_f; } + Pair(ImGuiID _key, void* _val_p) { key = _key; val_p = _val_p; } + }; + ImVector Data; + + // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) + // - Set***() functions find pair, insertion on demand if missing. + // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. + void Clear() { Data.clear(); } + IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; + IMGUI_API void SetInt(ImGuiID key, int val); + IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; + IMGUI_API void SetBool(ImGuiID key, bool val); + IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; + IMGUI_API void SetFloat(ImGuiID key, float val); + IMGUI_API void* GetVoidPtr(ImGuiID key) const; // default_val is NULL + IMGUI_API void SetVoidPtr(ImGuiID key, void* val); + + // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. + // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. + // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) + // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; + IMGUI_API int* GetIntRef(ImGuiID key, int default_val = 0); + IMGUI_API bool* GetBoolRef(ImGuiID key, bool default_val = false); + IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); + IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); + + // Use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); + + // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); +}; + +// Shared state of InputText(), passed to callback when a ImGuiInputTextFlags_Callback* flag is used and the corresponding callback is triggered. +struct ImGuiTextEditCallbackData +{ + ImGuiInputTextFlags EventFlag; // One of ImGuiInputTextFlags_Callback* // Read-only + ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only + void* UserData; // What user passed to InputText() // Read-only + bool ReadOnly; // Read-only mode // Read-only + + // CharFilter event: + ImWchar EventChar; // Character input // Read-write (replace character or set to zero) + + // Completion,History,Always events: + // If you modify the buffer contents make sure you update 'BufTextLen' and set 'BufDirty' to true. + ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only + char* Buf; // Current text buffer // Read-write (pointed data only, can't replace the actual pointer) + int BufTextLen; // Current text length in bytes // Read-write + int BufSize; // Maximum text length in bytes // Read-only + bool BufDirty; // Set if you modify Buf/BufTextLen!! // Write + int CursorPos; // // Read-write + int SelectionStart; // // Read-write (== to SelectionEnd when no selection) + int SelectionEnd; // // Read-write + + // NB: Helper functions for text manipulation. Calling those function loses selection. + IMGUI_API void DeleteChars(int pos, int bytes_count); + IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); + bool HasSelection() const { return SelectionStart != SelectionEnd; } +}; + +// Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). +// NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. +struct ImGuiSizeCallbackData +{ + void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints() + ImVec2 Pos; // Read-only. Window position, for reference. + ImVec2 CurrentSize; // Read-only. Current window size. + ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. +}; + +// Data payload for Drag and Drop operations +struct ImGuiPayload +{ + // Members + const void* Data; // Data (copied and owned by dear imgui) + int DataSize; // Data size + + // [Internal] + ImGuiID SourceId; // Source item id + ImGuiID SourceParentId; // Source parent id (if available) + int DataFrameCount; // Data timestamp + char DataType[12 + 1]; // Data type tag (short user-supplied string, 12 characters max) + bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) + bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. + + ImGuiPayload() { Clear(); } + void Clear() { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; } + bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; } + bool IsPreview() const { return Preview; } + bool IsDelivery() const { return Delivery; } +}; + +// Helpers macros to generate 32-bits encoded colors +#ifdef IMGUI_USE_BGRA_PACKED_COLOR +#define IM_COL32_R_SHIFT 16 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 0 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#else +#define IM_COL32_R_SHIFT 0 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 16 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#endif +#define IM_COL32(R,G,B,A) (((ImU32)(A)<>IM_COL32_R_SHIFT)&0xFF) * sc; Value.y = (float)((rgba>>IM_COL32_G_SHIFT)&0xFF) * sc; Value.z = (float)((rgba>>IM_COL32_B_SHIFT)&0xFF) * sc; Value.w = (float)((rgba>>IM_COL32_A_SHIFT)&0xFF) * sc; } + ImColor(float r, float g, float b, float a = 1.0f) { Value.x = r; Value.y = g; Value.z = b; Value.w = a; } + ImColor(const ImVec4& col) { Value = col; } + inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } + inline operator ImVec4() const { return Value; } + + // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. + inline void SetHSV(float h, float s, float v, float a = 1.0f){ ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); Value.w = a; } + static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r,g,b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r,g,b,a); } +}; + +// Helper: Manually clip large list of items. +// If you are submitting lots of evenly spaced items and you have a random access to the list, you can perform coarse clipping based on visibility to save yourself from processing those items at all. +// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. +// ImGui already clip items based on their bounds but it needs to measure text size to do so. Coarse clipping before submission makes this cost and your own data fetching/submission cost null. +// Usage: +// ImGuiListClipper clipper(1000); // we have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// - Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height (step skipped if we passed a known height as second arg to constructor). +// - Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. +// - (Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user call Step(). Does nothing and switch to Step 3.) +// - Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. +struct ImGuiListClipper +{ + float StartPosY; + float ItemsHeight; + int ItemsCount, StepNo, DisplayStart, DisplayEnd; + + // items_count: Use -1 to ignore (you can call Begin later). Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step). + // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). + // If you don't specify an items_height, you NEED to call Step(). If you specify items_height you may call the old Begin()/End() api directly, but prefer calling Step(). + ImGuiListClipper(int items_count = -1, float items_height = -1.0f) { Begin(items_count, items_height); } // NB: Begin() initialize every fields (as we allow user to call Begin/End multiple times on a same instance if they want). + ~ImGuiListClipper() { IM_ASSERT(ItemsCount == -1); } // Assert if user forgot to call End() or Step() until false. + + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. + IMGUI_API void Begin(int items_count, float items_height = -1.0f); // Automatically called by constructor if you passed 'items_count' or by Step() in Step 1. + IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. +}; + +//----------------------------------------------------------------------------- +// Draw List +// Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. +//----------------------------------------------------------------------------- + +// Draw callbacks for advanced uses. +// NB- You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering (you can poke into the draw list for that) +// Draw callback may be useful for example, A) Change your GPU render state, B) render a complex 3D scene inside a UI element (without an intermediate texture/render target), etc. +// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) cmd.UserCallback(parent_list, cmd); else RenderTriangles()' +typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); + +// Typically, 1 command = 1 GPU draw call (unless command is a callback) +struct ImDrawCmd +{ + unsigned int ElemCount; // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. + ImVec4 ClipRect; // Clipping rectangle (x1, y1, x2, y2) + ImTextureID TextureId; // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. + ImDrawCallback UserCallback; // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. + void* UserCallbackData; // The draw callback code can access this. + + ImDrawCmd() { ElemCount = 0; ClipRect.x = ClipRect.y = ClipRect.z = ClipRect.w = 0.0f; TextureId = NULL; UserCallback = NULL; UserCallbackData = NULL; } +}; + +// Vertex index (override with '#define ImDrawIdx unsigned int' inside in imconfig.h) +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; +#endif + +// Vertex layout +#ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT +struct ImDrawVert +{ + ImVec2 pos; + ImVec2 uv; + ImU32 col; +}; +#else +// You can override the vertex format layout by defining IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT in imconfig.h +// The code expect ImVec2 pos (8 bytes), ImVec2 uv (8 bytes), ImU32 col (4 bytes), but you can re-order them or add other fields as needed to simplify integration in your engine. +// The type has to be described within the macro (you can either declare the struct or use a typedef) +// NOTE: IMGUI DOESN'T CLEAR THE STRUCTURE AND DOESN'T CALL A CONSTRUCTOR SO ANY CUSTOM FIELD WILL BE UNINITIALIZED. IF YOU ADD EXTRA FIELDS (SUCH AS A 'Z' COORDINATES) YOU WILL NEED TO CLEAR THEM DURING RENDER OR TO IGNORE THEM. +IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; +#endif + +// Draw channels are used by the Columns API to "split" the render list into different channels while building, so items of each column can be batched together. +// You can also use them to simulate drawing layers and submit primitives in a different order than how they will be rendered. +struct ImDrawChannel +{ + ImVector CmdBuffer; + ImVector IdxBuffer; +}; + +enum ImDrawCornerFlags_ +{ + ImDrawCornerFlags_TopLeft = 1 << 0, // 0x1 + ImDrawCornerFlags_TopRight = 1 << 1, // 0x2 + ImDrawCornerFlags_BotLeft = 1 << 2, // 0x4 + ImDrawCornerFlags_BotRight = 1 << 3, // 0x8 + ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, // 0x3 + ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, // 0xC + ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, // 0x5 + ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, // 0xA + ImDrawCornerFlags_All = 0xF // In your function calls you may use ~0 (= all bits sets) instead of ImDrawCornerFlags_All, as a convenience +}; + +enum ImDrawListFlags_ +{ + ImDrawListFlags_AntiAliasedLines = 1 << 0, + ImDrawListFlags_AntiAliasedFill = 1 << 1 +}; + +// Draw command list +// This is the low-level list of polygons that ImGui functions are filling. At the end of the frame, all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. +// Each ImGui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to access the current window draw list and draw custom primitives. +// You can interleave normal ImGui:: calls and adding primitives to the current draw list. +// All positions are generally in pixel coordinates (top-left at (0,0), bottom-right at io.DisplaySize), however you are totally free to apply whatever transformation matrix to want to the data (if you apply such transformation you'll want to apply it to ClipRect as well) +// Important: Primitives are always added to the list and not culled (culling is done at higher-level by ImGui:: functions), if you use this API a lot consider coarse culling your drawn objects. +struct ImDrawList +{ + // This is what you have to render + ImVector CmdBuffer; // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback. + ImVector IdxBuffer; // Index buffer. Each command consume ImDrawCmd::ElemCount of those + ImVector VtxBuffer; // Vertex buffer. + + // [Internal, used while building lists] + ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. + const ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) + const char* _OwnerName; // Pointer to owner window's name for debugging + unsigned int _VtxCurrentIdx; // [Internal] == VtxBuffer.Size + ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImVector _ClipRectStack; // [Internal] + ImVector _TextureIdStack; // [Internal] + ImVector _Path; // [Internal] current path building + int _ChannelsCurrent; // [Internal] current channel number (0) + int _ChannelsCount; // [Internal] number of active channels (1+) + ImVector _Channels; // [Internal] draw channels for columns API (not resized down so _ChannelsCount may be smaller than _Channels.Size) + + // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) + ImDrawList(const ImDrawListSharedData* shared_data) { _Data = shared_data; _OwnerName = NULL; Clear(); } + ~ImDrawList() { ClearFreeMemory(); } + IMGUI_API void PushClipRect(ImVec2 clip_rect_min, ImVec2 clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) + IMGUI_API void PushClipRectFullScreen(); + IMGUI_API void PopClipRect(); + IMGUI_API void PushTextureID(ImTextureID texture_id); + IMGUI_API void PopTextureID(); + inline ImVec2 GetClipRectMin() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.x, cr.y); } + inline ImVec2 GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); } + + // Primitives + IMGUI_API void AddLine(const ImVec2& a, const ImVec2& b, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddRect(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding = 0.0f, int rounding_corners_flags = ImDrawCornerFlags_All, float thickness = 1.0f); // a: upper-left, b: lower-right, rounding_corners_flags: 4-bits corresponding to which corner to round + IMGUI_API void AddRectFilled(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding = 0.0f, int rounding_corners_flags = ImDrawCornerFlags_All); // a: upper-left, b: lower-right + IMGUI_API void AddRectFilledMultiColor(const ImVec2& a, const ImVec2& b, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); + IMGUI_API void AddQuad(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddQuadFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, ImU32 col); + IMGUI_API void AddTriangle(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddTriangleFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col); + IMGUI_API void AddCircle(const ImVec2& centre, float radius, ImU32 col, int num_segments = 12, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& centre, float radius, ImU32 col, int num_segments = 12); + IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); + IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); + IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a = ImVec2(0,0), const ImVec2& uv_b = ImVec2(1,1), ImU32 col = 0xFFFFFFFF); + IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a = ImVec2(0,0), const ImVec2& uv_b = ImVec2(1,0), const ImVec2& uv_c = ImVec2(1,1), const ImVec2& uv_d = ImVec2(0,1), ImU32 col = 0xFFFFFFFF); + IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col, float rounding, int rounding_corners = ImDrawCornerFlags_All); + IMGUI_API void AddPolyline(const ImVec2* points, const int num_points, ImU32 col, bool closed, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, const int num_points, ImU32 col); + IMGUI_API void AddBezierCurve(const ImVec2& pos0, const ImVec2& cp0, const ImVec2& cp1, const ImVec2& pos1, ImU32 col, float thickness, int num_segments = 0); + + // Stateful path API, add points then finish with PathFill() or PathStroke() + inline void PathClear() { _Path.resize(0); } + inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } + inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path[_Path.Size-1], &pos, 8) != 0) _Path.push_back(pos); } + inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); PathClear(); } + inline void PathStroke(ImU32 col, bool closed, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, closed, thickness); PathClear(); } + IMGUI_API void PathArcTo(const ImVec2& centre, float radius, float a_min, float a_max, int num_segments = 10); + IMGUI_API void PathArcToFast(const ImVec2& centre, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathBezierCurveTo(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, int num_segments = 0); + IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, int rounding_corners_flags = ImDrawCornerFlags_All); + + // Channels + // - Use to simulate layers. By switching channels to can render out-of-order (e.g. submit foreground primitives before background primitives) + // - Use to minimize draw calls (e.g. if going back-and-forth between multiple non-overlapping clipping rectangles, prefer to append into separate channels then merge at the end) + IMGUI_API void ChannelsSplit(int channels_count); + IMGUI_API void ChannelsMerge(); + IMGUI_API void ChannelsSetCurrent(int channel_index); + + // Advanced + IMGUI_API void AddCallback(ImDrawCallback callback, void* callback_data); // Your rendering function must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. + IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible + + // Internal helpers + // NB: all primitives needs to be reserved via PrimReserve() beforehand! + IMGUI_API void Clear(); + IMGUI_API void ClearFreeMemory(); + IMGUI_API void PrimReserve(int idx_count, int vtx_count); + IMGUI_API void PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col); // Axis aligned rectangle (composed of two triangles) + IMGUI_API void PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); + IMGUI_API void PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); + inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col){ _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } + inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } + inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } + IMGUI_API void UpdateClipRect(); + IMGUI_API void UpdateTextureID(); +}; + +// All draw data to render an ImGui frame +struct ImDrawData +{ + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + ImDrawList** CmdLists; + int CmdListsCount; + int TotalVtxCount; // For convenience, sum of all cmd_lists vtx_buffer.Size + int TotalIdxCount; // For convenience, sum of all cmd_lists idx_buffer.Size + + // Functions + ImDrawData() { Clear(); } + void Clear() { Valid = false; CmdLists = NULL; CmdListsCount = TotalVtxCount = TotalIdxCount = 0; } // Draw lists are owned by the ImGuiContext and only pointed to here. + IMGUI_API void DeIndexAllBuffers(); // For backward compatibility or convenience: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! + IMGUI_API void ScaleClipRects(const ImVec2& sc); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. +}; + +struct ImFontConfig +{ + void* FontData; // // TTF/OTF data + int FontDataSize; // // TTF/OTF data size + bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). + int FontNo; // 0 // Index of font within TTF/OTF file + float SizePixels; // // Size in pixels for rasterizer. + int OversampleH, OversampleV; // 3, 1 // Rasterize at higher quality for sub-pixel positioning. We don't use sub-pixel positions on the Y axis. + bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. + ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. + ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. + const ImWchar* GlyphRanges; // NULL // Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. + bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. + unsigned int RasterizerFlags; // 0x00 // Settings for custom font rasterizer (e.g. ImGuiFreeType). Leave as zero if you aren't using one. + float RasterizerMultiply; // 1.0f // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. + + // [Internal] + char Name[32]; // Name (strictly to ease debugging) + ImFont* DstFont; + + IMGUI_API ImFontConfig(); +}; + +struct ImFontGlyph +{ + ImWchar Codepoint; // 0x0000..0xFFFF + float AdvanceX; // Distance to next character (= data from font + ImFontConfig::GlyphExtraSpacing.x baked in) + float X0, Y0, X1, Y1; // Glyph corners + float U0, V0, U1, V1; // Texture coordinates +}; + +enum ImFontAtlasFlags_ +{ + ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two + ImFontAtlasFlags_NoMouseCursors = 1 << 1 // Don't build software mouse cursors into the atlas +}; + +// Load and rasterize multiple TTF/OTF fonts into a same texture. +// Sharing a texture for multiple fonts allows us to reduce the number of draw calls during rendering. +// We also add custom graphic data into the texture that serves for ImGui. +// 1. (Optional) Call AddFont*** functions. If you don't call any, the default font will be loaded for you. +// 2. Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. +// 3. Upload the pixels data into a texture within your graphics system. +// 4. Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture. This value will be passed back to you during rendering to identify the texture. +// IMPORTANT: If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the ImFont is build (when calling GetTextData*** or Build()). We only copy the pointer, not the data. +struct ImFontAtlas +{ + IMGUI_API ImFontAtlas(); + IMGUI_API ~ImFontAtlas(); + IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); + IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); + IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after Build(). Set font_cfg->FontDataOwnedByAtlas to false to keep ownership. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. + IMGUI_API void ClearTexData(); // Clear the CPU-side texture data. Saves RAM once the texture has been copied to graphics memory. + IMGUI_API void ClearInputData(); // Clear the input TTF data (inc sizes, glyph ranges) + IMGUI_API void ClearFonts(); // Clear the ImGui-side font data (glyphs storage, UV coordinates) + IMGUI_API void Clear(); // Clear all + + // Build atlas, retrieve pixel data. + // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). + // RGBA32 format is provided for convenience and compatibility, but note that unless you use CustomRect to draw color data, the RGB pixels emitted from Fonts will all be white (~75% of waste). + // Pitch = Width * BytesPerPixels + IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. + IMGUI_API void GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel + IMGUI_API void GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel + void SetTexID(ImTextureID id) { TexID = id; } + + //------------------------------------------- + // Glyph Ranges + //------------------------------------------- + + // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) + // NB: Make sure that your string are UTF-8 and NOT in your local code page. In C++11, you can create UTF-8 string literal using the u8"Hello world" syntax. See FAQ for details. + IMGUI_API const ImWchar* GetGlyphRangesDefault(); // Basic Latin, Extended Latin + IMGUI_API const ImWchar* GetGlyphRangesKorean(); // Default + Korean characters + IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChinese(); // Default + Japanese + full set of about 21000 CJK Unified Ideographs + IMGUI_API const ImWchar* GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters + IMGUI_API const ImWchar* GetGlyphRangesThai(); // Default + Thai characters + + // Helpers to build glyph ranges from text data. Feed your application strings/characters to it then call BuildRanges(). + struct GlyphRangesBuilder + { + ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) + GlyphRangesBuilder() { UsedChars.resize(0x10000 / 8); memset(UsedChars.Data, 0, 0x10000 / 8); } + bool GetBit(int n) { return (UsedChars[n >> 3] & (1 << (n & 7))) != 0; } + void SetBit(int n) { UsedChars[n >> 3] |= 1 << (n & 7); } // Set bit 'c' in the array + void AddChar(ImWchar c) { SetBit(c); } // Add character + IMGUI_API void AddText(const char* text, const char* text_end = NULL); // Add string (each character of the UTF-8 string are added) + IMGUI_API void AddRanges(const ImWchar* ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault) to force add all of ASCII/Latin+Ext + IMGUI_API void BuildRanges(ImVector* out_ranges); // Output new ranges + }; + + //------------------------------------------- + // Custom Rectangles/Glyphs API + //------------------------------------------- + + // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. After calling Build(), you can query the rectangle position and render your pixels. + // You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), so you can render e.g. custom colorful icons and use them as regular glyphs. + struct CustomRect + { + unsigned int ID; // Input // User ID. Use <0x10000 to map into a font glyph, >=0x10000 for other/internal/custom texture data. + unsigned short Width, Height; // Input // Desired rectangle dimension + unsigned short X, Y; // Output // Packed position in Atlas + float GlyphAdvanceX; // Input // For custom font glyphs only (ID<0x10000): glyph xadvance + ImVec2 GlyphOffset; // Input // For custom font glyphs only (ID<0x10000): glyph display offset + ImFont* Font; // Input // For custom font glyphs only (ID<0x10000): target font + CustomRect() { ID = 0xFFFFFFFF; Width = Height = 0; X = Y = 0xFFFF; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0,0); Font = NULL; } + bool IsPacked() const { return X != 0xFFFF; } + }; + + IMGUI_API int AddCustomRectRegular(unsigned int id, int width, int height); // Id needs to be >= 0x10000. Id >= 0x80000000 are reserved for ImGui and ImDrawList + IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0,0)); // Id needs to be < 0x10000 to register a rectangle to map into a specific font. + const CustomRect* GetCustomRectByIndex(int index) const { if (index < 0) return NULL; return &CustomRects[index]; } + + // Internals + IMGUI_API void CalcCustomRectUV(const CustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max); + IMGUI_API bool GetMouseCursorTexData(ImGuiMouseCursor cursor, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + + //------------------------------------------- + // Members + //------------------------------------------- + + ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) + ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. + int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. + int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. + + // [Internal] + // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. + unsigned char* TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight + unsigned int* TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 + int TexWidth; // Texture width calculated during Build(). + int TexHeight; // Texture height calculated during Build(). + ImVec2 TexUvScale; // = (1.0f/TexWidth, 1.0f/TexHeight) + ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel + ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. + ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. + ImVector ConfigData; // Internal data + int CustomRectIds[1]; // Identifiers of custom texture rectangle used by ImFontAtlas/ImDrawList +}; + +// Font runtime data and rendering +// ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32(). +struct ImFont +{ + // Members: Hot ~62/78 bytes + float FontSize; // // Height of characters, set during loading (don't change after loading) + float Scale; // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetFontScale() + ImVec2 DisplayOffset; // = (0.f,1.f) // Offset font rendering by xx pixels + ImVector Glyphs; // // All glyphs. + ImVector IndexAdvanceX; // // Sparse. Glyphs->AdvanceX in a directly indexable way (more cache-friendly, for CalcTextSize functions which are often bottleneck in large UI). + ImVector IndexLookup; // // Sparse. Index glyphs by Unicode code-point. + const ImFontGlyph* FallbackGlyph; // == FindGlyph(FontFallbackChar) + float FallbackAdvanceX; // == FallbackGlyph->AdvanceX + ImWchar FallbackChar; // = '?' // Replacement glyph if one isn't found. Only set via SetFallbackChar() + + // Members: Cold ~18/26 bytes + short ConfigDataCount; // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. + ImFontConfig* ConfigData; // // Pointer within ContainerAtlas->ConfigData + ImFontAtlas* ContainerAtlas; // // What we has been loaded into + float Ascent, Descent; // // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] + int MetricsTotalSurface;// // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) + + // Methods + IMGUI_API ImFont(); + IMGUI_API ~ImFont(); + IMGUI_API void ClearOutputData(); + IMGUI_API void BuildLookupTable(); + IMGUI_API const ImFontGlyph*FindGlyph(ImWchar c) const; + IMGUI_API void SetFallbackChar(ImWchar c); + float GetCharAdvance(ImWchar c) const { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } + bool IsLoaded() const { return ContainerAtlas != NULL; } + const char* GetDebugName() const { return ConfigData ? ConfigData->Name : ""; } + + // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. + // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL) const; // utf8 + IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const; + IMGUI_API void RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, unsigned short c) const; + IMGUI_API void RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false) const; + + // [Internal] + IMGUI_API void GrowIndex(int new_size); + IMGUI_API void AddGlyph(ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); + IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + typedef ImFontGlyph Glyph; // OBSOLETE 1.52+ +#endif +}; + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +// Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) +#ifdef IMGUI_INCLUDE_IMGUI_USER_H +#include "imgui_user.h" +#endif diff --git a/attachments/simple_engine/imgui/imgui_draw.cpp b/attachments/simple_engine/imgui/imgui_draw.cpp new file mode 100644 index 00000000..9ba63056 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui_draw.cpp @@ -0,0 +1,2943 @@ +// dear imgui, v1.60 WIP +// (drawing and font code) + +// Contains implementation for +// - Default styles +// - ImDrawList +// - ImDrawData +// - ImFontAtlas +// - ImFont +// - Default font data + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_internal.h" + +#include // vsnprintf, sscanf, printf +#if !defined(alloca) +#ifdef _WIN32 +#include // alloca +#if !defined(alloca) +#define alloca _alloca // for clang with MS Codegen +#endif +#elif defined(__GLIBC__) || defined(__sun) +#include // alloca +#else +#include // alloca +#endif +#endif + +#ifdef _MSC_VER +#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#define snprintf _snprintf +#endif + +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants ok. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference it. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // +#if __has_warning("-Wcomma") +#pragma clang diagnostic ignored "-Wcomma" // warning : possible misuse of comma operator here // +#endif +#if __has_warning("-Wreserved-id-macro") +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning : macro name is a reserved identifier // +#endif +#if __has_warning("-Wdouble-promotion") +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#endif +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'xxxx' to type 'xxxx' casts away qualifiers +#endif + +//------------------------------------------------------------------------- +// STB libraries implementation +//------------------------------------------------------------------------- + +//#define IMGUI_STB_NAMESPACE ImGuiStb +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION + +#ifdef IMGUI_STB_NAMESPACE +namespace IMGUI_STB_NAMESPACE +{ +#endif + +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4456) // declaration of 'xx' hides previous local declaration +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#endif + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] +#endif + +#define STBRP_ASSERT(x) IM_ASSERT(x) +#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +#define STBRP_STATIC +#define STB_RECT_PACK_IMPLEMENTATION +#endif +#include "stb_rect_pack.h" + +#define STBTT_malloc(x,u) ((void)(u), ImGui::MemAlloc(x)) +#define STBTT_free(x,u) ((void)(u), ImGui::MemFree(x)) +#define STBTT_assert(x) IM_ASSERT(x) +#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +#define STBTT_STATIC +#define STB_TRUETYPE_IMPLEMENTATION +#else +#define STBTT_DEF extern +#endif +#include "stb_truetype.h" + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +#ifdef IMGUI_STB_NAMESPACE +} // namespace ImGuiStb +using namespace IMGUI_STB_NAMESPACE; +#endif + +//----------------------------------------------------------------------------- +// Style functions +//----------------------------------------------------------------------------- + +void ImGui::StyleColorsDark(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + colors[ImGuiCol_ChildBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.29f, 0.48f, 0.54f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.29f, 0.48f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.25f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_CloseButton] = ImVec4(0.41f, 0.41f, 0.41f, 0.50f); + colors[ImGuiCol_CloseButtonHovered] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); + colors[ImGuiCol_CloseButtonActive] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); +} + +void ImGui::StyleColorsClassic(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.70f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); + colors[ImGuiCol_Border] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.47f, 0.47f, 0.69f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.41f, 0.64f, 0.69f); + colors[ImGuiCol_TitleBg] = ImVec4(0.27f, 0.27f, 0.54f, 0.83f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.32f, 0.32f, 0.63f, 0.87f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.40f, 0.40f, 0.55f, 0.80f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.40f, 0.40f, 0.80f, 0.30f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); + colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.48f, 0.71f, 0.79f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.46f, 0.54f, 0.80f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.40f, 0.40f, 0.90f, 0.45f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.45f, 0.45f, 0.90f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.53f, 0.53f, 0.87f, 0.80f); + colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.16f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); + colors[ImGuiCol_CloseButton] = ImVec4(0.50f, 0.50f, 0.90f, 0.50f); + colors[ImGuiCol_CloseButtonHovered] = ImVec4(0.70f, 0.70f, 0.90f, 0.60f); + colors[ImGuiCol_CloseButtonActive] = ImVec4(0.70f, 0.70f, 0.70f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); + colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); +} + +// Those light colors are better suited with a thicker font than the default one + FrameBorder +void ImGui::StyleColorsLight(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + //colors[ImGuiCol_TextHovered] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + //colors[ImGuiCol_TextActive] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.00f, 0.30f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(1.00f, 1.00f, 1.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.98f, 0.98f, 0.98f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 0.80f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.14f, 0.44f, 0.80f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.14f, 0.44f, 0.80f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.80f, 0.80f, 0.80f, 0.56f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_CloseButton] = ImVec4(0.59f, 0.59f, 0.59f, 0.50f); + colors[ImGuiCol_CloseButtonHovered] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); + colors[ImGuiCol_CloseButtonActive] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); +} + +//----------------------------------------------------------------------------- +// ImDrawListData +//----------------------------------------------------------------------------- + +ImDrawListSharedData::ImDrawListSharedData() +{ + Font = NULL; + FontSize = 0.0f; + CurveTessellationTol = 0.0f; + ClipRectFullscreen = ImVec4(-8192.0f, -8192.0f, +8192.0f, +8192.0f); + + // Const data + for (int i = 0; i < IM_ARRAYSIZE(CircleVtx12); i++) + { + const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(CircleVtx12); + CircleVtx12[i] = ImVec2(cosf(a), sinf(a)); + } +} + +//----------------------------------------------------------------------------- +// ImDrawList +//----------------------------------------------------------------------------- + +void ImDrawList::Clear() +{ + CmdBuffer.resize(0); + IdxBuffer.resize(0); + VtxBuffer.resize(0); + Flags = ImDrawListFlags_AntiAliasedLines | ImDrawListFlags_AntiAliasedFill; + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.resize(0); + _TextureIdStack.resize(0); + _Path.resize(0); + _ChannelsCurrent = 0; + _ChannelsCount = 1; + // NB: Do not clear channels so our allocations are re-used after the first frame. +} + +void ImDrawList::ClearFreeMemory() +{ + CmdBuffer.clear(); + IdxBuffer.clear(); + VtxBuffer.clear(); + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.clear(); + _TextureIdStack.clear(); + _Path.clear(); + _ChannelsCurrent = 0; + _ChannelsCount = 1; + for (int i = 0; i < _Channels.Size; i++) + { + if (i == 0) memset(&_Channels[0], 0, sizeof(_Channels[0])); // channel 0 is a copy of CmdBuffer/IdxBuffer, don't destruct again + _Channels[i].CmdBuffer.clear(); + _Channels[i].IdxBuffer.clear(); + } + _Channels.clear(); +} + +// Using macros because C++ is a terrible language, we want guaranteed inline, no code in header, and no overhead in Debug builds +#define GetCurrentClipRect() (_ClipRectStack.Size ? _ClipRectStack.Data[_ClipRectStack.Size-1] : _Data->ClipRectFullscreen) +#define GetCurrentTextureId() (_TextureIdStack.Size ? _TextureIdStack.Data[_TextureIdStack.Size-1] : NULL) + +void ImDrawList::AddDrawCmd() +{ + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = GetCurrentClipRect(); + draw_cmd.TextureId = GetCurrentTextureId(); + + IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); + CmdBuffer.push_back(draw_cmd); +} + +void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) +{ + ImDrawCmd* current_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; + if (!current_cmd || current_cmd->ElemCount != 0 || current_cmd->UserCallback != NULL) + { + AddDrawCmd(); + current_cmd = &CmdBuffer.back(); + } + current_cmd->UserCallback = callback; + current_cmd->UserCallbackData = callback_data; + + AddDrawCmd(); // Force a new command after us (see comment below) +} + +// Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. +// The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. +void ImDrawList::UpdateClipRect() +{ + // If current command is used with different settings we need to add a new command + const ImVec4 curr_clip_rect = GetCurrentClipRect(); + ImDrawCmd* curr_cmd = CmdBuffer.Size > 0 ? &CmdBuffer.Data[CmdBuffer.Size-1] : NULL; + if (!curr_cmd || (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) != 0) || curr_cmd->UserCallback != NULL) + { + AddDrawCmd(); + return; + } + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; + if (curr_cmd->ElemCount == 0 && prev_cmd && memcmp(&prev_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) == 0 && prev_cmd->TextureId == GetCurrentTextureId() && prev_cmd->UserCallback == NULL) + CmdBuffer.pop_back(); + else + curr_cmd->ClipRect = curr_clip_rect; +} + +void ImDrawList::UpdateTextureID() +{ + // If current command is used with different settings we need to add a new command + const ImTextureID curr_texture_id = GetCurrentTextureId(); + ImDrawCmd* curr_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; + if (!curr_cmd || (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != curr_texture_id) || curr_cmd->UserCallback != NULL) + { + AddDrawCmd(); + return; + } + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; + if (curr_cmd->ElemCount == 0 && prev_cmd && prev_cmd->TextureId == curr_texture_id && memcmp(&prev_cmd->ClipRect, &GetCurrentClipRect(), sizeof(ImVec4)) == 0 && prev_cmd->UserCallback == NULL) + CmdBuffer.pop_back(); + else + curr_cmd->TextureId = curr_texture_id; +} + +#undef GetCurrentClipRect +#undef GetCurrentTextureId + +// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) +void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect) +{ + ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); + if (intersect_with_current_clip_rect && _ClipRectStack.Size) + { + ImVec4 current = _ClipRectStack.Data[_ClipRectStack.Size-1]; + if (cr.x < current.x) cr.x = current.x; + if (cr.y < current.y) cr.y = current.y; + if (cr.z > current.z) cr.z = current.z; + if (cr.w > current.w) cr.w = current.w; + } + cr.z = ImMax(cr.x, cr.z); + cr.w = ImMax(cr.y, cr.w); + + _ClipRectStack.push_back(cr); + UpdateClipRect(); +} + +void ImDrawList::PushClipRectFullScreen() +{ + PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); +} + +void ImDrawList::PopClipRect() +{ + IM_ASSERT(_ClipRectStack.Size > 0); + _ClipRectStack.pop_back(); + UpdateClipRect(); +} + +void ImDrawList::PushTextureID(ImTextureID texture_id) +{ + _TextureIdStack.push_back(texture_id); + UpdateTextureID(); +} + +void ImDrawList::PopTextureID() +{ + IM_ASSERT(_TextureIdStack.Size > 0); + _TextureIdStack.pop_back(); + UpdateTextureID(); +} + +void ImDrawList::ChannelsSplit(int channels_count) +{ + IM_ASSERT(_ChannelsCurrent == 0 && _ChannelsCount == 1); + int old_channels_count = _Channels.Size; + if (old_channels_count < channels_count) + _Channels.resize(channels_count); + _ChannelsCount = channels_count; + + // _Channels[] (24/32 bytes each) hold storage that we'll swap with this->_CmdBuffer/_IdxBuffer + // The content of _Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. + // When we switch to the next channel, we'll copy _CmdBuffer/_IdxBuffer into _Channels[0] and then _Channels[1] into _CmdBuffer/_IdxBuffer + memset(&_Channels[0], 0, sizeof(ImDrawChannel)); + for (int i = 1; i < channels_count; i++) + { + if (i >= old_channels_count) + { + IM_PLACEMENT_NEW(&_Channels[i]) ImDrawChannel(); + } + else + { + _Channels[i].CmdBuffer.resize(0); + _Channels[i].IdxBuffer.resize(0); + } + if (_Channels[i].CmdBuffer.Size == 0) + { + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = _ClipRectStack.back(); + draw_cmd.TextureId = _TextureIdStack.back(); + _Channels[i].CmdBuffer.push_back(draw_cmd); + } + } +} + +void ImDrawList::ChannelsMerge() +{ + // Note that we never use or rely on channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. + if (_ChannelsCount <= 1) + return; + + ChannelsSetCurrent(0); + if (CmdBuffer.Size && CmdBuffer.back().ElemCount == 0) + CmdBuffer.pop_back(); + + int new_cmd_buffer_count = 0, new_idx_buffer_count = 0; + for (int i = 1; i < _ChannelsCount; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (ch.CmdBuffer.Size && ch.CmdBuffer.back().ElemCount == 0) + ch.CmdBuffer.pop_back(); + new_cmd_buffer_count += ch.CmdBuffer.Size; + new_idx_buffer_count += ch.IdxBuffer.Size; + } + CmdBuffer.resize(CmdBuffer.Size + new_cmd_buffer_count); + IdxBuffer.resize(IdxBuffer.Size + new_idx_buffer_count); + + ImDrawCmd* cmd_write = CmdBuffer.Data + CmdBuffer.Size - new_cmd_buffer_count; + _IdxWritePtr = IdxBuffer.Data + IdxBuffer.Size - new_idx_buffer_count; + for (int i = 1; i < _ChannelsCount; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (int sz = ch.CmdBuffer.Size) { memcpy(cmd_write, ch.CmdBuffer.Data, sz * sizeof(ImDrawCmd)); cmd_write += sz; } + if (int sz = ch.IdxBuffer.Size) { memcpy(_IdxWritePtr, ch.IdxBuffer.Data, sz * sizeof(ImDrawIdx)); _IdxWritePtr += sz; } + } + UpdateClipRect(); // We call this instead of AddDrawCmd(), so that empty channels won't produce an extra draw call. + _ChannelsCount = 1; +} + +void ImDrawList::ChannelsSetCurrent(int idx) +{ + IM_ASSERT(idx < _ChannelsCount); + if (_ChannelsCurrent == idx) return; + memcpy(&_Channels.Data[_ChannelsCurrent].CmdBuffer, &CmdBuffer, sizeof(CmdBuffer)); // copy 12 bytes, four times + memcpy(&_Channels.Data[_ChannelsCurrent].IdxBuffer, &IdxBuffer, sizeof(IdxBuffer)); + _ChannelsCurrent = idx; + memcpy(&CmdBuffer, &_Channels.Data[_ChannelsCurrent].CmdBuffer, sizeof(CmdBuffer)); + memcpy(&IdxBuffer, &_Channels.Data[_ChannelsCurrent].IdxBuffer, sizeof(IdxBuffer)); + _IdxWritePtr = IdxBuffer.Data + IdxBuffer.Size; +} + +// NB: this can be called with negative count for removing primitives (as long as the result does not underflow) +void ImDrawList::PrimReserve(int idx_count, int vtx_count) +{ + ImDrawCmd& draw_cmd = CmdBuffer.Data[CmdBuffer.Size-1]; + draw_cmd.ElemCount += idx_count; + + int vtx_buffer_old_size = VtxBuffer.Size; + VtxBuffer.resize(vtx_buffer_old_size + vtx_count); + _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size; + + int idx_buffer_old_size = IdxBuffer.Size; + IdxBuffer.resize(idx_buffer_old_size + idx_count); + _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; +} + +// Fully unrolled with inline call to keep our debug builds decently fast. +void ImDrawList::PrimRect(const ImVec2& a, const ImVec2& c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv(_Data->TexUvWhitePixel); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimRectUV(const ImVec2& a, const ImVec2& c, const ImVec2& uv_a, const ImVec2& uv_c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv_b(uv_c.x, uv_a.y), uv_d(uv_a.x, uv_c.y); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col) +{ + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +// TODO: Thickness anti-aliased lines cap are missing their AA fringe. +void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, bool closed, float thickness) +{ + if (points_count < 2) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + + int count = points_count; + if (!closed) + count = points_count-1; + + const bool thick_line = thickness > 1.0f; + if (Flags & ImDrawListFlags_AntiAliasedLines) + { + // Anti-aliased stroke + const float AA_SIZE = 1.0f; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + + const int idx_count = thick_line ? count*18 : count*12; + const int vtx_count = thick_line ? points_count*4 : points_count*3; + PrimReserve(idx_count, vtx_count); + + // Temporary buffer + ImVec2* temp_normals = (ImVec2*)alloca(points_count * (thick_line ? 5 : 3) * sizeof(ImVec2)); + ImVec2* temp_points = temp_normals + points_count; + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + ImVec2 diff = points[i2] - points[i1]; + diff *= ImInvLength(diff, 1.0f); + temp_normals[i1].x = diff.y; + temp_normals[i1].y = -diff.x; + } + if (!closed) + temp_normals[points_count-1] = temp_normals[points_count-2]; + + if (!thick_line) + { + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * AA_SIZE; + temp_points[1] = points[0] - temp_normals[0] * AA_SIZE; + temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * AA_SIZE; + temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * AA_SIZE; + } + + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + unsigned int idx2 = (i1+1) == points_count ? _VtxCurrentIdx : idx1+3; + + // Average normals + ImVec2 dm = (temp_normals[i1] + temp_normals[i2]) * 0.5f; + float dmr2 = dm.x*dm.x + dm.y*dm.y; + if (dmr2 > 0.000001f) + { + float scale = 1.0f / dmr2; + if (scale > 100.0f) scale = 100.0f; + dm *= scale; + } + dm *= AA_SIZE; + temp_points[i2*2+0] = points[i2] + dm; + temp_points[i2*2+1] = points[i2] - dm; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2+0); _IdxWritePtr[1] = (ImDrawIdx)(idx1+0); _IdxWritePtr[2] = (ImDrawIdx)(idx1+2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1+2); _IdxWritePtr[4] = (ImDrawIdx)(idx2+2); _IdxWritePtr[5] = (ImDrawIdx)(idx2+0); + _IdxWritePtr[6] = (ImDrawIdx)(idx2+1); _IdxWritePtr[7] = (ImDrawIdx)(idx1+1); _IdxWritePtr[8] = (ImDrawIdx)(idx1+0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1+0); _IdxWritePtr[10]= (ImDrawIdx)(idx2+0); _IdxWritePtr[11]= (ImDrawIdx)(idx2+1); + _IdxWritePtr += 12; + + idx1 = idx2; + } + + // Add vertexes + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = temp_points[i*2+0]; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; + _VtxWritePtr[2].pos = temp_points[i*2+1]; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col_trans; + _VtxWritePtr += 3; + } + } + else + { + const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); + temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); + temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[(points_count-1)*4+0] = points[points_count-1] + temp_normals[points_count-1] * (half_inner_thickness + AA_SIZE); + temp_points[(points_count-1)*4+1] = points[points_count-1] + temp_normals[points_count-1] * (half_inner_thickness); + temp_points[(points_count-1)*4+2] = points[points_count-1] - temp_normals[points_count-1] * (half_inner_thickness); + temp_points[(points_count-1)*4+3] = points[points_count-1] - temp_normals[points_count-1] * (half_inner_thickness + AA_SIZE); + } + + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + unsigned int idx2 = (i1+1) == points_count ? _VtxCurrentIdx : idx1+4; + + // Average normals + ImVec2 dm = (temp_normals[i1] + temp_normals[i2]) * 0.5f; + float dmr2 = dm.x*dm.x + dm.y*dm.y; + if (dmr2 > 0.000001f) + { + float scale = 1.0f / dmr2; + if (scale > 100.0f) scale = 100.0f; + dm *= scale; + } + ImVec2 dm_out = dm * (half_inner_thickness + AA_SIZE); + ImVec2 dm_in = dm * half_inner_thickness; + temp_points[i2*4+0] = points[i2] + dm_out; + temp_points[i2*4+1] = points[i2] + dm_in; + temp_points[i2*4+2] = points[i2] - dm_in; + temp_points[i2*4+3] = points[i2] - dm_out; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2+1); _IdxWritePtr[1] = (ImDrawIdx)(idx1+1); _IdxWritePtr[2] = (ImDrawIdx)(idx1+2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1+2); _IdxWritePtr[4] = (ImDrawIdx)(idx2+2); _IdxWritePtr[5] = (ImDrawIdx)(idx2+1); + _IdxWritePtr[6] = (ImDrawIdx)(idx2+1); _IdxWritePtr[7] = (ImDrawIdx)(idx1+1); _IdxWritePtr[8] = (ImDrawIdx)(idx1+0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1+0); _IdxWritePtr[10] = (ImDrawIdx)(idx2+0); _IdxWritePtr[11] = (ImDrawIdx)(idx2+1); + _IdxWritePtr[12] = (ImDrawIdx)(idx2+2); _IdxWritePtr[13] = (ImDrawIdx)(idx1+2); _IdxWritePtr[14] = (ImDrawIdx)(idx1+3); + _IdxWritePtr[15] = (ImDrawIdx)(idx1+3); _IdxWritePtr[16] = (ImDrawIdx)(idx2+3); _IdxWritePtr[17] = (ImDrawIdx)(idx2+2); + _IdxWritePtr += 18; + + idx1 = idx2; + } + + // Add vertexes + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i*4+0]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col_trans; + _VtxWritePtr[1].pos = temp_points[i*4+1]; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = temp_points[i*4+2]; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = temp_points[i*4+3]; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col_trans; + _VtxWritePtr += 4; + } + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Stroke + const int idx_count = count*6; + const int vtx_count = count*4; // FIXME-OPT: Not sharing edges + PrimReserve(idx_count, vtx_count); + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + const ImVec2& p1 = points[i1]; + const ImVec2& p2 = points[i2]; + ImVec2 diff = p2 - p1; + diff *= ImInvLength(diff, 1.0f); + + const float dx = diff.x * (thickness * 0.5f); + const float dy = diff.y * (thickness * 0.5f); + _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx+1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx+2); + _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx+2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx+3); + _IdxWritePtr += 6; + _VtxCurrentIdx += 4; + } + } +} + +void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + const ImVec2 uv = _Data->TexUvWhitePixel; + + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = 1.0f; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count-2)*3 + points_count*6; + const int vtx_count = (points_count*2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx+1; + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx+((i-1)<<1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx+(i<<1)); + _IdxWritePtr += 3; + } + + // Compute normals + ImVec2* temp_normals = (ImVec2*)alloca(points_count * sizeof(ImVec2)); + for (int i0 = points_count-1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + ImVec2 diff = p1 - p0; + diff *= ImInvLength(diff, 1.0f); + temp_normals[i0].x = diff.y; + temp_normals[i0].y = -diff.x; + } + + for (int i0 = points_count-1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + ImVec2 dm = (n0 + n1) * 0.5f; + float dmr2 = dm.x*dm.x + dm.y*dm.y; + if (dmr2 > 0.000001f) + { + float scale = 1.0f / dmr2; + if (scale > 100.0f) scale = 100.0f; + dm *= scale; + } + dm *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos = (points[i1] - dm); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos = (points[i1] + dm); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx+(i1<<1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx+(i0<<1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx+(i0<<1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx+(i0<<1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx+(i1<<1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx+(i1<<1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count-2)*3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx+i-1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx+i); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +void ImDrawList::PathArcToFast(const ImVec2& centre, float radius, int a_min_of_12, int a_max_of_12) +{ + if (radius == 0.0f || a_min_of_12 > a_max_of_12) + { + _Path.push_back(centre); + return; + } + _Path.reserve(_Path.Size + (a_max_of_12 - a_min_of_12 + 1)); + for (int a = a_min_of_12; a <= a_max_of_12; a++) + { + const ImVec2& c = _Data->CircleVtx12[a % IM_ARRAYSIZE(_Data->CircleVtx12)]; + _Path.push_back(ImVec2(centre.x + c.x * radius, centre.y + c.y * radius)); + } +} + +void ImDrawList::PathArcTo(const ImVec2& centre, float radius, float a_min, float a_max, int num_segments) +{ + if (radius == 0.0f) + { + _Path.push_back(centre); + return; + } + _Path.reserve(_Path.Size + (num_segments + 1)); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + _Path.push_back(ImVec2(centre.x + cosf(a) * radius, centre.y + sinf(a) * radius)); + } +} + +static void PathBezierToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); + float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2+d3) * (d2+d3) < tess_tol * (dx*dx + dy*dy)) + { + path->push_back(ImVec2(x4, y4)); + } + else if (level < 10) + { + float x12 = (x1+x2)*0.5f, y12 = (y1+y2)*0.5f; + float x23 = (x2+x3)*0.5f, y23 = (y2+y3)*0.5f; + float x34 = (x3+x4)*0.5f, y34 = (y3+y4)*0.5f; + float x123 = (x12+x23)*0.5f, y123 = (y12+y23)*0.5f; + float x234 = (x23+x34)*0.5f, y234 = (y23+y34)*0.5f; + float x1234 = (x123+x234)*0.5f, y1234 = (y123+y234)*0.5f; + + PathBezierToCasteljau(path, x1,y1, x12,y12, x123,y123, x1234,y1234, tess_tol, level+1); + PathBezierToCasteljau(path, x1234,y1234, x234,y234, x34,y34, x4,y4, tess_tol, level+1); + } +} + +void ImDrawList::PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + // Auto-tessellated + PathBezierToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + { + float t = t_step * i_step; + float u = 1.0f - t; + float w1 = u*u*u; + float w2 = 3*u*u*t; + float w3 = 3*u*t*t; + float w4 = t*t*t; + _Path.push_back(ImVec2(w1*p1.x + w2*p2.x + w3*p3.x + w4*p4.x, w1*p1.y + w2*p2.y + w3*p3.y + w4*p4.y)); + } + } +} + +void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, int rounding_corners) +{ + rounding = ImMin(rounding, fabsf(b.x - a.x) * ( ((rounding_corners & ImDrawCornerFlags_Top) == ImDrawCornerFlags_Top) || ((rounding_corners & ImDrawCornerFlags_Bot) == ImDrawCornerFlags_Bot) ? 0.5f : 1.0f ) - 1.0f); + rounding = ImMin(rounding, fabsf(b.y - a.y) * ( ((rounding_corners & ImDrawCornerFlags_Left) == ImDrawCornerFlags_Left) || ((rounding_corners & ImDrawCornerFlags_Right) == ImDrawCornerFlags_Right) ? 0.5f : 1.0f ) - 1.0f); + + if (rounding <= 0.0f || rounding_corners == 0) + { + PathLineTo(a); + PathLineTo(ImVec2(b.x, a.y)); + PathLineTo(b); + PathLineTo(ImVec2(a.x, b.y)); + } + else + { + const float rounding_tl = (rounding_corners & ImDrawCornerFlags_TopLeft) ? rounding : 0.0f; + const float rounding_tr = (rounding_corners & ImDrawCornerFlags_TopRight) ? rounding : 0.0f; + const float rounding_br = (rounding_corners & ImDrawCornerFlags_BotRight) ? rounding : 0.0f; + const float rounding_bl = (rounding_corners & ImDrawCornerFlags_BotLeft) ? rounding : 0.0f; + PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); + PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); + PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); + PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6); + } +} + +void ImDrawList::AddLine(const ImVec2& a, const ImVec2& b, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + PathLineTo(a + ImVec2(0.5f,0.5f)); + PathLineTo(b + ImVec2(0.5f,0.5f)); + PathStroke(col, false, thickness); +} + +// a: upper-left, b: lower-right. we don't render 1 px sized rectangles properly. +void ImDrawList::AddRect(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, int rounding_corners_flags, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (Flags & ImDrawListFlags_AntiAliasedLines) + PathRect(a + ImVec2(0.5f,0.5f), b - ImVec2(0.50f,0.50f), rounding, rounding_corners_flags); + else + PathRect(a + ImVec2(0.5f,0.5f), b - ImVec2(0.49f,0.49f), rounding, rounding_corners_flags); // Better looking lower-right corner and rounded non-AA shapes. + PathStroke(col, true, thickness); +} + +void ImDrawList::AddRectFilled(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, int rounding_corners_flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (rounding > 0.0f) + { + PathRect(a, b, rounding, rounding_corners_flags); + PathFillConvex(col); + } + else + { + PrimReserve(6, 4); + PrimRect(a, b, col); + } +} + +void ImDrawList::AddRectFilledMultiColor(const ImVec2& a, const ImVec2& c, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) +{ + if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + PrimReserve(6, 4); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+1)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+2)); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+2)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+3)); + PrimWriteVtx(a, uv, col_upr_left); + PrimWriteVtx(ImVec2(c.x, a.y), uv, col_upr_right); + PrimWriteVtx(c, uv, col_bot_right); + PrimWriteVtx(ImVec2(a.x, c.y), uv, col_bot_left); +} + +void ImDrawList::AddQuad(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathLineTo(d); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddQuadFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathLineTo(d); + PathFillConvex(col); +} + +void ImDrawList::AddTriangle(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddTriangleFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathFillConvex(col); +} + +void ImDrawList::AddCircle(const ImVec2& centre, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const float a_max = IM_PI*2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(centre, radius-0.5f, 0.0f, a_max, num_segments); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddCircleFilled(const ImVec2& centre, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const float a_max = IM_PI*2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(centre, radius, 0.0f, a_max, num_segments); + PathFillConvex(col); +} + +void ImDrawList::AddBezierCurve(const ImVec2& pos0, const ImVec2& cp0, const ImVec2& cp1, const ImVec2& pos1, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(pos0); + PathBezierCurveTo(cp0, cp1, pos1, num_segments); + PathStroke(col, false, thickness); +} + +void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (text_end == NULL) + text_end = text_begin + strlen(text_begin); + if (text_begin == text_end) + return; + + // Pull default font/size from the shared ImDrawListSharedData instance + if (font == NULL) + font = _Data->Font; + if (font_size == 0.0f) + font_size = _Data->FontSize; + + IM_ASSERT(font->ContainerAtlas->TexID == _TextureIdStack.back()); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. + + ImVec4 clip_rect = _ClipRectStack.back(); + if (cpu_fine_clip_rect) + { + clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect->x); + clip_rect.y = ImMax(clip_rect.y, cpu_fine_clip_rect->y); + clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z); + clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w); + } + font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL); +} + +void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +{ + AddText(NULL, 0.0f, pos, col, text_begin, text_end); +} + +void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimRectUV(a, b, uv_a, uv_b, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimQuadUV(a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col, float rounding, int rounding_corners) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (rounding <= 0.0f || (rounding_corners & ImDrawCornerFlags_All) == 0) + { + AddImage(user_texture_id, a, b, uv_a, uv_b, col); + return; + } + + const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + if (push_texture_id) + PushTextureID(user_texture_id); + + int vert_start_idx = VtxBuffer.Size; + PathRect(a, b, rounding, rounding_corners); + PathFillConvex(col); + int vert_end_idx = VtxBuffer.Size; + ImGui::ShadeVertsLinearUV(VtxBuffer.Data + vert_start_idx, VtxBuffer.Data + vert_end_idx, a, b, uv_a, uv_b, true); + + if (push_texture_id) + PopTextureID(); +} + +//----------------------------------------------------------------------------- +// ImDrawData +//----------------------------------------------------------------------------- + +// For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! +void ImDrawData::DeIndexAllBuffers() +{ + ImVector new_vtx_buffer; + TotalVtxCount = TotalIdxCount = 0; + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + if (cmd_list->IdxBuffer.empty()) + continue; + new_vtx_buffer.resize(cmd_list->IdxBuffer.Size); + for (int j = 0; j < cmd_list->IdxBuffer.Size; j++) + new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]]; + cmd_list->VtxBuffer.swap(new_vtx_buffer); + cmd_list->IdxBuffer.resize(0); + TotalVtxCount += cmd_list->VtxBuffer.Size; + } +} + +// Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. +void ImDrawData::ScaleClipRects(const ImVec2& scale) +{ + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + ImDrawCmd* cmd = &cmd_list->CmdBuffer[cmd_i]; + cmd->ClipRect = ImVec4(cmd->ClipRect.x * scale.x, cmd->ClipRect.y * scale.y, cmd->ClipRect.z * scale.x, cmd->ClipRect.w * scale.y); + } + } +} + +//----------------------------------------------------------------------------- +// Shade functions +//----------------------------------------------------------------------------- + +// Generic linear color gradient, write to RGB fields, leave A untouched. +void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawVert* vert_start, ImDrawVert* vert_end, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1) +{ + ImVec2 gradient_extent = gradient_p1 - gradient_p0; + float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); + for (ImDrawVert* vert = vert_start; vert < vert_end; vert++) + { + float d = ImDot(vert->pos - gradient_p0, gradient_extent); + float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + int r = ImLerp((int)(col0 >> IM_COL32_R_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col0 >> IM_COL32_G_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col0 >> IM_COL32_B_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_B_SHIFT) & 0xFF, t); + vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); + } +} + +// Scan and shade backward from the end of given vertices. Assume vertices are text only (= vert_start..vert_end going left to right) so we can break as soon as we are out the gradient bounds. +void ImGui::ShadeVertsLinearAlphaGradientForLeftToRightText(ImDrawVert* vert_start, ImDrawVert* vert_end, float gradient_p0_x, float gradient_p1_x) +{ + float gradient_extent_x = gradient_p1_x - gradient_p0_x; + float gradient_inv_length2 = 1.0f / (gradient_extent_x * gradient_extent_x); + int full_alpha_count = 0; + for (ImDrawVert* vert = vert_end - 1; vert >= vert_start; vert--) + { + float d = (vert->pos.x - gradient_p0_x) * (gradient_extent_x); + float alpha_mul = 1.0f - ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + if (alpha_mul >= 1.0f && ++full_alpha_count > 2) + return; // Early out + int a = (int)(((vert->col >> IM_COL32_A_SHIFT) & 0xFF) * alpha_mul); + vert->col = (vert->col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); + } +} + +// Distribute UV over (a, b) rectangle +void ImGui::ShadeVertsLinearUV(ImDrawVert* vert_start, ImDrawVert* vert_end, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp) +{ + const ImVec2 size = b - a; + const ImVec2 uv_size = uv_b - uv_a; + const ImVec2 scale = ImVec2( + size.x != 0.0f ? (uv_size.x / size.x) : 0.0f, + size.y != 0.0f ? (uv_size.y / size.y) : 0.0f); + + if (clamp) + { + const ImVec2 min = ImMin(uv_a, uv_b); + const ImVec2 max = ImMax(uv_a, uv_b); + + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = ImClamp(uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale), min, max); + } + else + { + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale); + } +} + +//----------------------------------------------------------------------------- +// ImFontConfig +//----------------------------------------------------------------------------- + +ImFontConfig::ImFontConfig() +{ + FontData = NULL; + FontDataSize = 0; + FontDataOwnedByAtlas = true; + FontNo = 0; + SizePixels = 0.0f; + OversampleH = 3; + OversampleV = 1; + PixelSnapH = false; + GlyphExtraSpacing = ImVec2(0.0f, 0.0f); + GlyphOffset = ImVec2(0.0f, 0.0f); + GlyphRanges = NULL; + MergeMode = false; + RasterizerFlags = 0x00; + RasterizerMultiply = 1.0f; + memset(Name, 0, sizeof(Name)); + DstFont = NULL; +} + +//----------------------------------------------------------------------------- +// ImFontAtlas +//----------------------------------------------------------------------------- + +// A work of art lies ahead! (. = white layer, X = black layer, others are blank) +// The white texels on the top left are the ones we'll use everywhere in ImGui to render filled shapes. +const int FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF = 90; +const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; +const unsigned int FONT_ATLAS_DEFAULT_TEX_DATA_ID = 0x80000000; +static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = +{ + "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX" + "..- -X.....X- X.X - X.X -X.....X - X.....X" + "--- -XXX.XXX- X...X - X...X -X....X - X....X" + "X - X.X - X.....X - X.....X -X...X - X...X" + "XX - X.X -X.......X- X.......X -X..X.X - X.X..X" + "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X" + "X..X - X.X - X.X - X.X -XX X.X - X.X XX" + "X...X - X.X - X.X - XX X.X XX - X.X - X.X " + "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X " + "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X " + "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X " + "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X " + "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X " + "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X " + "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X " + "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X " + "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX " + "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------" + "X.X X..X - -X.......X- X.......X - XX XX - " + "XX X..X - - X.....X - X.....X - X.X X.X - " + " X..X - X...X - X...X - X..X X..X - " + " XX - X.X - X.X - X...XXXXXXXXXXXXX...X - " + "------------ - X - X -X.....................X- " + " ----------------------------------- X...XXXXXXXXXXXXX...X - " + " - X..X X..X - " + " - X.X X.X - " + " - XX XX - " +}; + +static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_Count_][3] = +{ + // Pos ........ Size ......... Offset ...... + { ImVec2(0,3), ImVec2(12,19), ImVec2( 0, 0) }, // ImGuiMouseCursor_Arrow + { ImVec2(13,0), ImVec2(7,16), ImVec2( 4, 8) }, // ImGuiMouseCursor_TextInput + { ImVec2(31,0), ImVec2(23,23), ImVec2(11,11) }, // ImGuiMouseCursor_ResizeAll + { ImVec2(21,0), ImVec2( 9,23), ImVec2( 5,11) }, // ImGuiMouseCursor_ResizeNS + { ImVec2(55,18),ImVec2(23, 9), ImVec2(11, 5) }, // ImGuiMouseCursor_ResizeEW + { ImVec2(73,0), ImVec2(17,17), ImVec2( 9, 9) }, // ImGuiMouseCursor_ResizeNESW + { ImVec2(55,0), ImVec2(17,17), ImVec2( 9, 9) }, // ImGuiMouseCursor_ResizeNWSE +}; + +ImFontAtlas::ImFontAtlas() +{ + Flags = 0x00; + TexID = NULL; + TexDesiredWidth = 0; + TexGlyphPadding = 1; + + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; + TexWidth = TexHeight = 0; + TexUvScale = ImVec2(0.0f, 0.0f); + TexUvWhitePixel = ImVec2(0.0f, 0.0f); + for (int n = 0; n < IM_ARRAYSIZE(CustomRectIds); n++) + CustomRectIds[n] = -1; +} + +ImFontAtlas::~ImFontAtlas() +{ + Clear(); +} + +void ImFontAtlas::ClearInputData() +{ + for (int i = 0; i < ConfigData.Size; i++) + if (ConfigData[i].FontData && ConfigData[i].FontDataOwnedByAtlas) + { + ImGui::MemFree(ConfigData[i].FontData); + ConfigData[i].FontData = NULL; + } + + // When clearing this we lose access to the font name and other information used to build the font. + for (int i = 0; i < Fonts.Size; i++) + if (Fonts[i]->ConfigData >= ConfigData.Data && Fonts[i]->ConfigData < ConfigData.Data + ConfigData.Size) + { + Fonts[i]->ConfigData = NULL; + Fonts[i]->ConfigDataCount = 0; + } + ConfigData.clear(); + CustomRects.clear(); + for (int n = 0; n < IM_ARRAYSIZE(CustomRectIds); n++) + CustomRectIds[n] = -1; +} + +void ImFontAtlas::ClearTexData() +{ + if (TexPixelsAlpha8) + ImGui::MemFree(TexPixelsAlpha8); + if (TexPixelsRGBA32) + ImGui::MemFree(TexPixelsRGBA32); + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; +} + +void ImFontAtlas::ClearFonts() +{ + for (int i = 0; i < Fonts.Size; i++) + IM_DELETE(Fonts[i]); + Fonts.clear(); +} + +void ImFontAtlas::Clear() +{ + ClearInputData(); + ClearTexData(); + ClearFonts(); +} + +void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +{ + // Build atlas on demand + if (TexPixelsAlpha8 == NULL) + { + if (ConfigData.empty()) + AddFontDefault(); + Build(); + } + + *out_pixels = TexPixelsAlpha8; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 1; +} + +void ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +{ + // Convert to RGBA32 format on demand + // Although it is likely to be the most commonly used format, our font rendering is 1 channel / 8 bpp + if (!TexPixelsRGBA32) + { + unsigned char* pixels = NULL; + GetTexDataAsAlpha8(&pixels, NULL, NULL); + if (pixels) + { + TexPixelsRGBA32 = (unsigned int*)ImGui::MemAlloc((size_t)(TexWidth * TexHeight * 4)); + const unsigned char* src = pixels; + unsigned int* dst = TexPixelsRGBA32; + for (int n = TexWidth * TexHeight; n > 0; n--) + *dst++ = IM_COL32(255, 255, 255, (unsigned int)(*src++)); + } + } + + *out_pixels = (unsigned char*)TexPixelsRGBA32; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 4; +} + +ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) +{ + IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0); + IM_ASSERT(font_cfg->SizePixels > 0.0f); + + // Create new font + if (!font_cfg->MergeMode) + Fonts.push_back(IM_NEW(ImFont)); + else + IM_ASSERT(!Fonts.empty()); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. + + ConfigData.push_back(*font_cfg); + ImFontConfig& new_font_cfg = ConfigData.back(); + if (!new_font_cfg.DstFont) + new_font_cfg.DstFont = Fonts.back(); + if (!new_font_cfg.FontDataOwnedByAtlas) + { + new_font_cfg.FontData = ImGui::MemAlloc(new_font_cfg.FontDataSize); + new_font_cfg.FontDataOwnedByAtlas = true; + memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize); + } + + // Invalidate texture + ClearTexData(); + return new_font_cfg.DstFont; +} + +// Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder) +static unsigned int stb_decompress_length(unsigned char *input); +static unsigned int stb_decompress(unsigned char *output, unsigned char *i, unsigned int length); +static const char* GetDefaultCompressedFontDataTTFBase85(); +static unsigned int Decode85Byte(char c) { return c >= '\\' ? c-36 : c-35; } +static void Decode85(const unsigned char* src, unsigned char* dst) +{ + while (*src) + { + unsigned int tmp = Decode85Byte(src[0]) + 85*(Decode85Byte(src[1]) + 85*(Decode85Byte(src[2]) + 85*(Decode85Byte(src[3]) + 85*Decode85Byte(src[4])))); + dst[0] = ((tmp >> 0) & 0xFF); dst[1] = ((tmp >> 8) & 0xFF); dst[2] = ((tmp >> 16) & 0xFF); dst[3] = ((tmp >> 24) & 0xFF); // We can't assume little-endianness. + src += 5; + dst += 4; + } +} + +// Load embedded ProggyClean.ttf at size 13, disable oversampling +ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) +{ + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (!font_cfg_template) + { + font_cfg.OversampleH = font_cfg.OversampleV = 1; + font_cfg.PixelSnapH = true; + } + if (font_cfg.Name[0] == '\0') strcpy(font_cfg.Name, "ProggyClean.ttf, 13px"); + if (font_cfg.SizePixels <= 0.0f) font_cfg.SizePixels = 13.0f; + + const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); + ImFont* font = AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, GetGlyphRangesDefault()); + return font; +} + +ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + int data_size = 0; + void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0); + if (!data) + { + IM_ASSERT(0); // Could not load file. + return NULL; + } + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (font_cfg.Name[0] == '\0') + { + // Store a short copy of filename into into the font name for convenience + const char* p; + for (p = filename + strlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} + snprintf(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); + } + return AddFontFromMemoryTTF(data, data_size, size_pixels, &font_cfg, glyph_ranges); +} + +// NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). +ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* ttf_data, int ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + font_cfg.FontData = ttf_data; + font_cfg.FontDataSize = ttf_size; + font_cfg.SizePixels = size_pixels; + if (glyph_ranges) + font_cfg.GlyphRanges = glyph_ranges; + return AddFont(&font_cfg); +} + +ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + const unsigned int buf_decompressed_size = stb_decompress_length((unsigned char*)compressed_ttf_data); + unsigned char* buf_decompressed_data = (unsigned char *)ImGui::MemAlloc(buf_decompressed_size); + stb_decompress(buf_decompressed_data, (unsigned char*)compressed_ttf_data, (unsigned int)compressed_ttf_size); + + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + font_cfg.FontDataOwnedByAtlas = true; + return AddFontFromMemoryTTF(buf_decompressed_data, (int)buf_decompressed_size, size_pixels, &font_cfg, glyph_ranges); +} + +ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed_ttf_data_base85, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) +{ + int compressed_ttf_size = (((int)strlen(compressed_ttf_data_base85) + 4) / 5) * 4; + void* compressed_ttf = ImGui::MemAlloc((size_t)compressed_ttf_size); + Decode85((const unsigned char*)compressed_ttf_data_base85, (unsigned char*)compressed_ttf); + ImFont* font = AddFontFromMemoryCompressedTTF(compressed_ttf, compressed_ttf_size, size_pixels, font_cfg, glyph_ranges); + ImGui::MemFree(compressed_ttf); + return font; +} + +int ImFontAtlas::AddCustomRectRegular(unsigned int id, int width, int height) +{ + IM_ASSERT(id >= 0x10000); + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + CustomRect r; + r.ID = id; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} + +int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset) +{ + IM_ASSERT(font != NULL); + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + CustomRect r; + r.ID = id; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + r.GlyphAdvanceX = advance_x; + r.GlyphOffset = offset; + r.Font = font; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} + +void ImFontAtlas::CalcCustomRectUV(const CustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) +{ + IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates + IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed + *out_uv_min = ImVec2((float)rect->X * TexUvScale.x, (float)rect->Y * TexUvScale.y); + *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y); +} + +bool ImFontAtlas::GetMouseCursorTexData(ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) +{ + if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_Count_) + return false; + if (Flags & ImFontAtlasFlags_NoMouseCursors) + return false; + + ImFontAtlas::CustomRect& r = CustomRects[CustomRectIds[0]]; + IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); + ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r.X, (float)r.Y); + ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; + *out_size = size; + *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; + out_uv_border[0] = (pos) * TexUvScale; + out_uv_border[1] = (pos + size) * TexUvScale; + pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; + out_uv_fill[0] = (pos) * TexUvScale; + out_uv_fill[1] = (pos + size) * TexUvScale; + return true; +} + +bool ImFontAtlas::Build() +{ + return ImFontAtlasBuildWithStbTruetype(this); +} + +void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) +{ + for (unsigned int i = 0; i < 256; i++) + { + unsigned int value = (unsigned int)(i * in_brighten_factor); + out_table[i] = value > 255 ? 255 : (value & 0xFF); + } +} + +void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride) +{ + unsigned char* data = pixels + x + y * stride; + for (int j = h; j > 0; j--, data += stride) + for (int i = 0; i < w; i++) + data[i] = table[data[i]]; +} + +bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) +{ + IM_ASSERT(atlas->ConfigData.Size > 0); + + ImFontAtlasBuildRegisterDefaultCustomRects(atlas); + + atlas->TexID = NULL; + atlas->TexWidth = atlas->TexHeight = 0; + atlas->TexUvScale = ImVec2(0.0f, 0.0f); + atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f); + atlas->ClearTexData(); + + // Count glyphs/ranges + int total_glyphs_count = 0; + int total_ranges_count = 0; + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + if (!cfg.GlyphRanges) + cfg.GlyphRanges = atlas->GetGlyphRangesDefault(); + for (const ImWchar* in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, total_ranges_count++) + total_glyphs_count += (in_range[1] - in_range[0]) + 1; + } + + // We need a width for the skyline algorithm. Using a dumb heuristic here to decide of width. User can override TexDesiredWidth and TexGlyphPadding if they wish. + // Width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. + atlas->TexWidth = (atlas->TexDesiredWidth > 0) ? atlas->TexDesiredWidth : (total_glyphs_count > 4000) ? 4096 : (total_glyphs_count > 2000) ? 2048 : (total_glyphs_count > 1000) ? 1024 : 512; + atlas->TexHeight = 0; + + // Start packing + const int max_tex_height = 1024*32; + stbtt_pack_context spc = {}; + if (!stbtt_PackBegin(&spc, NULL, atlas->TexWidth, max_tex_height, 0, atlas->TexGlyphPadding, NULL)) + return false; + stbtt_PackSetOversampling(&spc, 1, 1); + + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); + + // Initialize font information (so we can error without any cleanup) + struct ImFontTempBuildData + { + stbtt_fontinfo FontInfo; + stbrp_rect* Rects; + int RectsCount; + stbtt_pack_range* Ranges; + int RangesCount; + }; + ImFontTempBuildData* tmp_array = (ImFontTempBuildData*)ImGui::MemAlloc((size_t)atlas->ConfigData.Size * sizeof(ImFontTempBuildData)); + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas)); + + const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo); + IM_ASSERT(font_offset >= 0); + if (!stbtt_InitFont(&tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset)) + { + atlas->TexWidth = atlas->TexHeight = 0; // Reset output on failure + ImGui::MemFree(tmp_array); + return false; + } + } + + // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0) + int buf_packedchars_n = 0, buf_rects_n = 0, buf_ranges_n = 0; + stbtt_packedchar* buf_packedchars = (stbtt_packedchar*)ImGui::MemAlloc(total_glyphs_count * sizeof(stbtt_packedchar)); + stbrp_rect* buf_rects = (stbrp_rect*)ImGui::MemAlloc(total_glyphs_count * sizeof(stbrp_rect)); + stbtt_pack_range* buf_ranges = (stbtt_pack_range*)ImGui::MemAlloc(total_ranges_count * sizeof(stbtt_pack_range)); + memset(buf_packedchars, 0, total_glyphs_count * sizeof(stbtt_packedchar)); + memset(buf_rects, 0, total_glyphs_count * sizeof(stbrp_rect)); // Unnecessary but let's clear this for the sake of sanity. + memset(buf_ranges, 0, total_ranges_count * sizeof(stbtt_pack_range)); + + // First font pass: pack all glyphs (no rendering at this point, we are working with rectangles in an infinitely tall texture at this point) + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + + // Setup ranges + int font_glyphs_count = 0; + int font_ranges_count = 0; + for (const ImWchar* in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, font_ranges_count++) + font_glyphs_count += (in_range[1] - in_range[0]) + 1; + tmp.Ranges = buf_ranges + buf_ranges_n; + tmp.RangesCount = font_ranges_count; + buf_ranges_n += font_ranges_count; + for (int i = 0; i < font_ranges_count; i++) + { + const ImWchar* in_range = &cfg.GlyphRanges[i * 2]; + stbtt_pack_range& range = tmp.Ranges[i]; + range.font_size = cfg.SizePixels; + range.first_unicode_codepoint_in_range = in_range[0]; + range.num_chars = (in_range[1] - in_range[0]) + 1; + range.chardata_for_range = buf_packedchars + buf_packedchars_n; + buf_packedchars_n += range.num_chars; + } + + // Pack + tmp.Rects = buf_rects + buf_rects_n; + tmp.RectsCount = font_glyphs_count; + buf_rects_n += font_glyphs_count; + stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV); + int n = stbtt_PackFontRangesGatherRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects); + IM_ASSERT(n == font_glyphs_count); + stbrp_pack_rects((stbrp_context*)spc.pack_info, tmp.Rects, n); + + // Extend texture height + for (int i = 0; i < n; i++) + if (tmp.Rects[i].was_packed) + atlas->TexHeight = ImMax(atlas->TexHeight, tmp.Rects[i].y + tmp.Rects[i].h); + } + IM_ASSERT(buf_rects_n == total_glyphs_count); + IM_ASSERT(buf_packedchars_n == total_glyphs_count); + IM_ASSERT(buf_ranges_n == total_ranges_count); + + // Create texture + atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight); + atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexPixelsAlpha8 = (unsigned char*)ImGui::MemAlloc(atlas->TexWidth * atlas->TexHeight); + memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight); + spc.pixels = atlas->TexPixelsAlpha8; + spc.height = atlas->TexHeight; + + // Second pass: render font characters + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV); + stbtt_PackFontRangesRenderIntoRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects); + if (cfg.RasterizerMultiply != 1.0f) + { + unsigned char multiply_table[256]; + ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply); + for (const stbrp_rect* r = tmp.Rects; r != tmp.Rects + tmp.RectsCount; r++) + if (r->was_packed) + ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, spc.pixels, r->x, r->y, r->w, r->h, spc.stride_in_bytes); + } + tmp.Rects = NULL; + } + + // End packing + stbtt_PackEnd(&spc); + ImGui::MemFree(buf_rects); + buf_rects = NULL; + + // Third pass: setup ImFont and glyphs for runtime + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + ImFont* dst_font = cfg.DstFont; // We can have multiple input fonts writing into a same destination font (when using MergeMode=true) + + const float font_scale = stbtt_ScaleForPixelHeight(&tmp.FontInfo, cfg.SizePixels); + int unscaled_ascent, unscaled_descent, unscaled_line_gap; + stbtt_GetFontVMetrics(&tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); + + const float ascent = unscaled_ascent * font_scale; + const float descent = unscaled_descent * font_scale; + ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); + const float off_x = cfg.GlyphOffset.x; + const float off_y = cfg.GlyphOffset.y + (float)(int)(dst_font->Ascent + 0.5f); + + for (int i = 0; i < tmp.RangesCount; i++) + { + stbtt_pack_range& range = tmp.Ranges[i]; + for (int char_idx = 0; char_idx < range.num_chars; char_idx += 1) + { + const stbtt_packedchar& pc = range.chardata_for_range[char_idx]; + if (!pc.x0 && !pc.x1 && !pc.y0 && !pc.y1) + continue; + + const int codepoint = range.first_unicode_codepoint_in_range + char_idx; + if (cfg.MergeMode && dst_font->FindGlyph((unsigned short)codepoint)) + continue; + + stbtt_aligned_quad q; + float dummy_x = 0.0f, dummy_y = 0.0f; + stbtt_GetPackedQuad(range.chardata_for_range, atlas->TexWidth, atlas->TexHeight, char_idx, &dummy_x, &dummy_y, &q, 0); + dst_font->AddGlyph((ImWchar)codepoint, q.x0 + off_x, q.y0 + off_y, q.x1 + off_x, q.y1 + off_y, q.s0, q.t0, q.s1, q.t1, pc.xadvance); + } + } + } + + // Cleanup temporaries + ImGui::MemFree(buf_packedchars); + ImGui::MemFree(buf_ranges); + ImGui::MemFree(tmp_array); + + ImFontAtlasBuildFinish(atlas); + + return true; +} + +void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas) +{ + if (atlas->CustomRectIds[0] >= 0) + return; + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF*2+1, FONT_ATLAS_DEFAULT_TEX_DATA_H); + else + atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, 2, 2); +} + +void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) +{ + if (!font_config->MergeMode) + { + font->ClearOutputData(); + font->FontSize = font_config->SizePixels; + font->ConfigData = font_config; + font->ContainerAtlas = atlas; + font->Ascent = ascent; + font->Descent = descent; + } + font->ConfigDataCount++; +} + +void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* pack_context_opaque) +{ + stbrp_context* pack_context = (stbrp_context*)pack_context_opaque; + + ImVector& user_rects = atlas->CustomRects; + IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. + + ImVector pack_rects; + pack_rects.resize(user_rects.Size); + memset(pack_rects.Data, 0, sizeof(stbrp_rect) * user_rects.Size); + for (int i = 0; i < user_rects.Size; i++) + { + pack_rects[i].w = user_rects[i].Width; + pack_rects[i].h = user_rects[i].Height; + } + stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); + for (int i = 0; i < pack_rects.Size; i++) + if (pack_rects[i].was_packed) + { + user_rects[i].X = pack_rects[i].x; + user_rects[i].Y = pack_rects[i].y; + IM_ASSERT(pack_rects[i].w == user_rects[i].Width && pack_rects[i].h == user_rects[i].Height); + atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); + } +} + +static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas) +{ + IM_ASSERT(atlas->CustomRectIds[0] >= 0); + IM_ASSERT(atlas->TexPixelsAlpha8 != NULL); + ImFontAtlas::CustomRect& r = atlas->CustomRects[atlas->CustomRectIds[0]]; + IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); + IM_ASSERT(r.IsPacked()); + + const int w = atlas->TexWidth; + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + { + // Render/copy pixels + IM_ASSERT(r.Width == FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * 2 + 1 && r.Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); + for (int y = 0, n = 0; y < FONT_ATLAS_DEFAULT_TEX_DATA_H; y++) + for (int x = 0; x < FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF; x++, n++) + { + const int offset0 = (int)(r.X + x) + (int)(r.Y + y) * w; + const int offset1 = offset0 + FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; + atlas->TexPixelsAlpha8[offset0] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == '.' ? 0xFF : 0x00; + atlas->TexPixelsAlpha8[offset1] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == 'X' ? 0xFF : 0x00; + } + } + else + { + IM_ASSERT(r.Width == 2 && r.Height == 2); + const int offset = (int)(r.X) + (int)(r.Y) * w; + atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + } + atlas->TexUvWhitePixel = ImVec2((r.X + 0.5f) * atlas->TexUvScale.x, (r.Y + 0.5f) * atlas->TexUvScale.y); +} + +void ImFontAtlasBuildFinish(ImFontAtlas* atlas) +{ + // Render into our custom data block + ImFontAtlasBuildRenderDefaultTexData(atlas); + + // Register custom rectangle glyphs + for (int i = 0; i < atlas->CustomRects.Size; i++) + { + const ImFontAtlas::CustomRect& r = atlas->CustomRects[i]; + if (r.Font == NULL || r.ID > 0x10000) + continue; + + IM_ASSERT(r.Font->ContainerAtlas == atlas); + ImVec2 uv0, uv1; + atlas->CalcCustomRectUV(&r, &uv0, &uv1); + r.Font->AddGlyph((ImWchar)r.ID, r.GlyphOffset.x, r.GlyphOffset.y, r.GlyphOffset.x + r.Width, r.GlyphOffset.y + r.Height, uv0.x, uv0.y, uv1.x, uv1.y, r.GlyphAdvanceX); + } + + // Build all fonts lookup tables + for (int i = 0; i < atlas->Fonts.Size; i++) + atlas->Fonts[i]->BuildLookupTable(); +} + +// Retrieve list of range (2 int per range, values are inclusive) +const ImWchar* ImFontAtlas::GetGlyphRangesDefault() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesKorean() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3131, 0x3163, // Korean alphabets + 0xAC00, 0xD79D, // Korean characters + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesChinese() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0x4e00, 0x9FAF, // CJK Ideograms + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesJapanese() +{ + // Store the 1946 ideograms code points as successive offsets from the initial unicode codepoint 0x4E00. Each offset has an implicit +1. + // This encoding is designed to helps us reduce the source code size. + // FIXME: Source a list of the revised 2136 joyo kanji list from 2010 and rebuild this. + // The current list was sourced from http://theinstructionlimit.com/author/renaudbedardrenaudbedard/page/3 + // Note that you may use ImFontAtlas::GlyphRangesBuilder to create your own ranges, by merging existing ranges or adding new characters. + static const short offsets_from_0x4E00[] = + { + -1,0,1,3,0,0,0,0,1,0,5,1,1,0,7,4,6,10,0,1,9,9,7,1,3,19,1,10,7,1,0,1,0,5,1,0,6,4,2,6,0,0,12,6,8,0,3,5,0,1,0,9,0,0,8,1,1,3,4,5,13,0,0,8,2,17, + 4,3,1,1,9,6,0,0,0,2,1,3,2,22,1,9,11,1,13,1,3,12,0,5,9,2,0,6,12,5,3,12,4,1,2,16,1,1,4,6,5,3,0,6,13,15,5,12,8,14,0,0,6,15,3,6,0,18,8,1,6,14,1, + 5,4,12,24,3,13,12,10,24,0,0,0,1,0,1,1,2,9,10,2,2,0,0,3,3,1,0,3,8,0,3,2,4,4,1,6,11,10,14,6,15,3,4,15,1,0,0,5,2,2,0,0,1,6,5,5,6,0,3,6,5,0,0,1,0, + 11,2,2,8,4,7,0,10,0,1,2,17,19,3,0,2,5,0,6,2,4,4,6,1,1,11,2,0,3,1,2,1,2,10,7,6,3,16,0,8,24,0,0,3,1,1,3,0,1,6,0,0,0,2,0,1,5,15,0,1,0,0,2,11,19, + 1,4,19,7,6,5,1,0,0,0,0,5,1,0,1,9,0,0,5,0,2,0,1,0,3,0,11,3,0,2,0,0,0,0,0,9,3,6,4,12,0,14,0,0,29,10,8,0,14,37,13,0,31,16,19,0,8,30,1,20,8,3,48, + 21,1,0,12,0,10,44,34,42,54,11,18,82,0,2,1,2,12,1,0,6,2,17,2,12,7,0,7,17,4,2,6,24,23,8,23,39,2,16,23,1,0,5,1,2,15,14,5,6,2,11,0,8,6,2,2,2,14, + 20,4,15,3,4,11,10,10,2,5,2,1,30,2,1,0,0,22,5,5,0,3,1,5,4,1,0,0,2,2,21,1,5,1,2,16,2,1,3,4,0,8,4,0,0,5,14,11,2,16,1,13,1,7,0,22,15,3,1,22,7,14, + 22,19,11,24,18,46,10,20,64,45,3,2,0,4,5,0,1,4,25,1,0,0,2,10,0,0,0,1,0,1,2,0,0,9,1,2,0,0,0,2,5,2,1,1,5,5,8,1,1,1,5,1,4,9,1,3,0,1,0,1,1,2,0,0, + 2,0,1,8,22,8,1,0,0,0,0,4,2,1,0,9,8,5,0,9,1,30,24,2,6,4,39,0,14,5,16,6,26,179,0,2,1,1,0,0,0,5,2,9,6,0,2,5,16,7,5,1,1,0,2,4,4,7,15,13,14,0,0, + 3,0,1,0,0,0,2,1,6,4,5,1,4,9,0,3,1,8,0,0,10,5,0,43,0,2,6,8,4,0,2,0,0,9,6,0,9,3,1,6,20,14,6,1,4,0,7,2,3,0,2,0,5,0,3,1,0,3,9,7,0,3,4,0,4,9,1,6,0, + 9,0,0,2,3,10,9,28,3,6,2,4,1,2,32,4,1,18,2,0,3,1,5,30,10,0,2,2,2,0,7,9,8,11,10,11,7,2,13,7,5,10,0,3,40,2,0,1,6,12,0,4,5,1,5,11,11,21,4,8,3,7, + 8,8,33,5,23,0,0,19,8,8,2,3,0,6,1,1,1,5,1,27,4,2,5,0,3,5,6,3,1,0,3,1,12,5,3,3,2,0,7,7,2,1,0,4,0,1,1,2,0,10,10,6,2,5,9,7,5,15,15,21,6,11,5,20, + 4,3,5,5,2,5,0,2,1,0,1,7,28,0,9,0,5,12,5,5,18,30,0,12,3,3,21,16,25,32,9,3,14,11,24,5,66,9,1,2,0,5,9,1,5,1,8,0,8,3,3,0,1,15,1,4,8,1,2,7,0,7,2, + 8,3,7,5,3,7,10,2,1,0,0,2,25,0,6,4,0,10,0,4,2,4,1,12,5,38,4,0,4,1,10,5,9,4,0,14,4,2,5,18,20,21,1,3,0,5,0,7,0,3,7,1,3,1,1,8,1,0,0,0,3,2,5,2,11, + 6,0,13,1,3,9,1,12,0,16,6,2,1,0,2,1,12,6,13,11,2,0,28,1,7,8,14,13,8,13,0,2,0,5,4,8,10,2,37,42,19,6,6,7,4,14,11,18,14,80,7,6,0,4,72,12,36,27, + 7,7,0,14,17,19,164,27,0,5,10,7,3,13,6,14,0,2,2,5,3,0,6,13,0,0,10,29,0,4,0,3,13,0,3,1,6,51,1,5,28,2,0,8,0,20,2,4,0,25,2,10,13,10,0,16,4,0,1,0, + 2,1,7,0,1,8,11,0,0,1,2,7,2,23,11,6,6,4,16,2,2,2,0,22,9,3,3,5,2,0,15,16,21,2,9,20,15,15,5,3,9,1,0,0,1,7,7,5,4,2,2,2,38,24,14,0,0,15,5,6,24,14, + 5,5,11,0,21,12,0,3,8,4,11,1,8,0,11,27,7,2,4,9,21,59,0,1,39,3,60,62,3,0,12,11,0,3,30,11,0,13,88,4,15,5,28,13,1,4,48,17,17,4,28,32,46,0,16,0, + 18,11,1,8,6,38,11,2,6,11,38,2,0,45,3,11,2,7,8,4,30,14,17,2,1,1,65,18,12,16,4,2,45,123,12,56,33,1,4,3,4,7,0,0,0,3,2,0,16,4,2,4,2,0,7,4,5,2,26, + 2,25,6,11,6,1,16,2,6,17,77,15,3,35,0,1,0,5,1,0,38,16,6,3,12,3,3,3,0,9,3,1,3,5,2,9,0,18,0,25,1,3,32,1,72,46,6,2,7,1,3,14,17,0,28,1,40,13,0,20, + 15,40,6,38,24,12,43,1,1,9,0,12,6,0,6,2,4,19,3,7,1,48,0,9,5,0,5,6,9,6,10,15,2,11,19,3,9,2,0,1,10,1,27,8,1,3,6,1,14,0,26,0,27,16,3,4,9,6,2,23, + 9,10,5,25,2,1,6,1,1,48,15,9,15,14,3,4,26,60,29,13,37,21,1,6,4,0,2,11,22,23,16,16,2,2,1,3,0,5,1,6,4,0,0,4,0,0,8,3,0,2,5,0,7,1,7,3,13,2,4,10, + 3,0,2,31,0,18,3,0,12,10,4,1,0,7,5,7,0,5,4,12,2,22,10,4,2,15,2,8,9,0,23,2,197,51,3,1,1,4,13,4,3,21,4,19,3,10,5,40,0,4,1,1,10,4,1,27,34,7,21, + 2,17,2,9,6,4,2,3,0,4,2,7,8,2,5,1,15,21,3,4,4,2,2,17,22,1,5,22,4,26,7,0,32,1,11,42,15,4,1,2,5,0,19,3,1,8,6,0,10,1,9,2,13,30,8,2,24,17,19,1,4, + 4,25,13,0,10,16,11,39,18,8,5,30,82,1,6,8,18,77,11,13,20,75,11,112,78,33,3,0,0,60,17,84,9,1,1,12,30,10,49,5,32,158,178,5,5,6,3,3,1,3,1,4,7,6, + 19,31,21,0,2,9,5,6,27,4,9,8,1,76,18,12,1,4,0,3,3,6,3,12,2,8,30,16,2,25,1,5,5,4,3,0,6,10,2,3,1,0,5,1,19,3,0,8,1,5,2,6,0,0,0,19,1,2,0,5,1,2,5, + 1,3,7,0,4,12,7,3,10,22,0,9,5,1,0,2,20,1,1,3,23,30,3,9,9,1,4,191,14,3,15,6,8,50,0,1,0,0,4,0,0,1,0,2,4,2,0,2,3,0,2,0,2,2,8,7,0,1,1,1,3,3,17,11, + 91,1,9,3,2,13,4,24,15,41,3,13,3,1,20,4,125,29,30,1,0,4,12,2,21,4,5,5,19,11,0,13,11,86,2,18,0,7,1,8,8,2,2,22,1,2,6,5,2,0,1,2,8,0,2,0,5,2,1,0, + 2,10,2,0,5,9,2,1,2,0,1,0,4,0,0,10,2,5,3,0,6,1,0,1,4,4,33,3,13,17,3,18,6,4,7,1,5,78,0,4,1,13,7,1,8,1,0,35,27,15,3,0,0,0,1,11,5,41,38,15,22,6, + 14,14,2,1,11,6,20,63,5,8,27,7,11,2,2,40,58,23,50,54,56,293,8,8,1,5,1,14,0,1,12,37,89,8,8,8,2,10,6,0,0,0,4,5,2,1,0,1,1,2,7,0,3,3,0,4,6,0,3,2, + 19,3,8,0,0,0,4,4,16,0,4,1,5,1,3,0,3,4,6,2,17,10,10,31,6,4,3,6,10,126,7,3,2,2,0,9,0,0,5,20,13,0,15,0,6,0,2,5,8,64,50,3,2,12,2,9,0,0,11,8,20, + 109,2,18,23,0,0,9,61,3,0,28,41,77,27,19,17,81,5,2,14,5,83,57,252,14,154,263,14,20,8,13,6,57,39,38, + }; + static ImWchar base_ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + }; + static bool full_ranges_unpacked = false; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(offsets_from_0x4E00)*2 + 1]; + if (!full_ranges_unpacked) + { + // Unpack + int codepoint = 0x4e00; + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + ImWchar* dst = full_ranges + IM_ARRAYSIZE(base_ranges);; + for (int n = 0; n < IM_ARRAYSIZE(offsets_from_0x4E00); n++, dst += 2) + dst[0] = dst[1] = (ImWchar)(codepoint += (offsets_from_0x4E00[n] + 1)); + dst[0] = 0; + full_ranges_unpacked = true; + } + return &full_ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesCyrillic() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0400, 0x052F, // Cyrillic + Cyrillic Supplement + 0x2DE0, 0x2DFF, // Cyrillic Extended-A + 0xA640, 0xA69F, // Cyrillic Extended-B + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesThai() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x2010, 0x205E, // Punctuations + 0x0E00, 0x0E7F, // Thai + 0, + }; + return &ranges[0]; +} + +//----------------------------------------------------------------------------- +// ImFontAtlas::GlyphRangesBuilder +//----------------------------------------------------------------------------- + +void ImFontAtlas::GlyphRangesBuilder::AddText(const char* text, const char* text_end) +{ + while (text_end ? (text < text_end) : *text) + { + unsigned int c = 0; + int c_len = ImTextCharFromUtf8(&c, text, text_end); + text += c_len; + if (c_len == 0) + break; + if (c < 0x10000) + AddChar((ImWchar)c); + } +} + +void ImFontAtlas::GlyphRangesBuilder::AddRanges(const ImWchar* ranges) +{ + for (; ranges[0]; ranges += 2) + for (ImWchar c = ranges[0]; c <= ranges[1]; c++) + AddChar(c); +} + +void ImFontAtlas::GlyphRangesBuilder::BuildRanges(ImVector* out_ranges) +{ + for (int n = 0; n < 0x10000; n++) + if (GetBit(n)) + { + out_ranges->push_back((ImWchar)n); + while (n < 0x10000 && GetBit(n + 1)) + n++; + out_ranges->push_back((ImWchar)n); + } + out_ranges->push_back(0); +} + +//----------------------------------------------------------------------------- +// ImFont +//----------------------------------------------------------------------------- + +ImFont::ImFont() +{ + Scale = 1.0f; + FallbackChar = (ImWchar)'?'; + DisplayOffset = ImVec2(0.0f, 1.0f); + ClearOutputData(); +} + +ImFont::~ImFont() +{ + // Invalidate active font so that the user gets a clear crash instead of a dangling pointer. + // If you want to delete fonts you need to do it between Render() and NewFrame(). + // FIXME-CLEANUP + /* + ImGuiContext& g = *GImGui; + if (g.Font == this) + g.Font = NULL; + */ + ClearOutputData(); +} + +void ImFont::ClearOutputData() +{ + FontSize = 0.0f; + Glyphs.clear(); + IndexAdvanceX.clear(); + IndexLookup.clear(); + FallbackGlyph = NULL; + FallbackAdvanceX = 0.0f; + ConfigDataCount = 0; + ConfigData = NULL; + ContainerAtlas = NULL; + Ascent = Descent = 0.0f; + MetricsTotalSurface = 0; +} + +void ImFont::BuildLookupTable() +{ + int max_codepoint = 0; + for (int i = 0; i != Glyphs.Size; i++) + max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint); + + IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved + IndexAdvanceX.clear(); + IndexLookup.clear(); + GrowIndex(max_codepoint + 1); + for (int i = 0; i < Glyphs.Size; i++) + { + int codepoint = (int)Glyphs[i].Codepoint; + IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX; + IndexLookup[codepoint] = (unsigned short)i; + } + + // Create a glyph to handle TAB + // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) + if (FindGlyph((unsigned short)' ')) + { + if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& tab_glyph = Glyphs.back(); + tab_glyph = *FindGlyph((unsigned short)' '); + tab_glyph.Codepoint = '\t'; + tab_glyph.AdvanceX *= 4; + IndexAdvanceX[(int)tab_glyph.Codepoint] = (float)tab_glyph.AdvanceX; + IndexLookup[(int)tab_glyph.Codepoint] = (unsigned short)(Glyphs.Size-1); + } + + FallbackGlyph = NULL; + FallbackGlyph = FindGlyph(FallbackChar); + FallbackAdvanceX = FallbackGlyph ? FallbackGlyph->AdvanceX : 0.0f; + for (int i = 0; i < max_codepoint + 1; i++) + if (IndexAdvanceX[i] < 0.0f) + IndexAdvanceX[i] = FallbackAdvanceX; +} + +void ImFont::SetFallbackChar(ImWchar c) +{ + FallbackChar = c; + BuildLookupTable(); +} + +void ImFont::GrowIndex(int new_size) +{ + IM_ASSERT(IndexAdvanceX.Size == IndexLookup.Size); + if (new_size <= IndexLookup.Size) + return; + IndexAdvanceX.resize(new_size, -1.0f); + IndexLookup.resize(new_size, (unsigned short)-1); +} + +void ImFont::AddGlyph(ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) +{ + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& glyph = Glyphs.back(); + glyph.Codepoint = (ImWchar)codepoint; + glyph.X0 = x0; + glyph.Y0 = y0; + glyph.X1 = x1; + glyph.Y1 = y1; + glyph.U0 = u0; + glyph.V0 = v0; + glyph.U1 = u1; + glyph.V1 = v1; + glyph.AdvanceX = advance_x + ConfigData->GlyphExtraSpacing.x; // Bake spacing into AdvanceX + + if (ConfigData->PixelSnapH) + glyph.AdvanceX = (float)(int)(glyph.AdvanceX + 0.5f); + + // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round) + MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + 1.99f) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + 1.99f); +} + +void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) +{ + IM_ASSERT(IndexLookup.Size > 0); // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function. + int index_size = IndexLookup.Size; + + if (dst < index_size && IndexLookup.Data[dst] == (unsigned short)-1 && !overwrite_dst) // 'dst' already exists + return; + if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op + return; + + GrowIndex(dst + 1); + IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (unsigned short)-1; + IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; +} + +const ImFontGlyph* ImFont::FindGlyph(ImWchar c) const +{ + if (c < IndexLookup.Size) + { + const unsigned short i = IndexLookup[c]; + if (i != (unsigned short)-1) + return &Glyphs.Data[i]; + } + return FallbackGlyph; +} + +const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const +{ + // Simple word-wrapping for English, not full-featured. Please submit failing cases! + // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) + + // For references, possible wrap point marked with ^ + // "aaa bbb, ccc,ddd. eee fff. ggg!" + // ^ ^ ^ ^ ^__ ^ ^ + + // List of hardcoded separators: .,;!?'" + + // Skip extra blanks after a line returns (that includes not counting them in width computation) + // e.g. "Hello world" --> "Hello" "World" + + // Cut words that cannot possibly fit within one line. + // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" + + float line_width = 0.0f; + float word_width = 0.0f; + float blank_width = 0.0f; + wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters + + const char* word_end = text; + const char* prev_word_end = NULL; + bool inside_word = true; + + const char* s = text; + while (s < text_end) + { + unsigned int c = (unsigned int)*s; + const char* next_s; + if (c < 0x80) + next_s = s + 1; + else + next_s = s + ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) + break; + + if (c < 32) + { + if (c == '\n') + { + line_width = word_width = blank_width = 0.0f; + inside_word = true; + s = next_s; + continue; + } + if (c == '\r') + { + s = next_s; + continue; + } + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX[(int)c] : FallbackAdvanceX); + if (ImCharIsSpace(c)) + { + if (inside_word) + { + line_width += blank_width; + blank_width = 0.0f; + word_end = s; + } + blank_width += char_width; + inside_word = false; + } + else + { + word_width += char_width; + if (inside_word) + { + word_end = next_s; + } + else + { + prev_word_end = word_end; + line_width += word_width + blank_width; + word_width = blank_width = 0.0f; + } + + // Allow wrapping after punctuation. + inside_word = !(c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\"'); + } + + // We ignore blank width at the end of the line (they can be skipped) + if (line_width + word_width >= wrap_width) + { + // Words that cannot possibly fit within an entire line will be cut anywhere. + if (word_width < wrap_width) + s = prev_word_end ? prev_word_end : word_end; + break; + } + + s = next_s; + } + + return s; +} + +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const +{ + if (!text_end) + text_end = text_begin + strlen(text_begin); // FIXME-OPT: Need to avoid this. + + const float line_height = size; + const float scale = size / FontSize; + + ImVec2 text_size = ImVec2(0,0); + float line_width = 0.0f; + + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + const char* s = text_begin; + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + if (text_size.x < line_width) + text_size.x = line_width; + text_size.y += line_height; + line_width = 0.0f; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (ImCharIsSpace(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source + const char* prev_s = s; + unsigned int c = (unsigned int)*s; + if (c < 0x80) + { + s += 1; + } + else + { + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) // Malformed UTF-8? + break; + } + + if (c < 32) + { + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + continue; + } + if (c == '\r') + continue; + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX[(int)c] : FallbackAdvanceX) * scale; + if (line_width + char_width >= max_width) + { + s = prev_s; + break; + } + + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (line_width > 0 || text_size.y == 0.0f) + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +void ImFont::RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, unsigned short c) const +{ + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') // Match behavior of RenderText(), those 4 codepoints are hard-coded. + return; + if (const ImFontGlyph* glyph = FindGlyph(c)) + { + float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; + pos.x = (float)(int)pos.x + DisplayOffset.x; + pos.y = (float)(int)pos.y + DisplayOffset.y; + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(ImVec2(pos.x + glyph->X0 * scale, pos.y + glyph->Y0 * scale), ImVec2(pos.x + glyph->X1 * scale, pos.y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); + } +} + +void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const +{ + if (!text_end) + text_end = text_begin + strlen(text_begin); // ImGui functions generally already provides a valid text_end, so this is merely to handle direct calls. + + // Align to be pixel perfect + pos.x = (float)(int)pos.x + DisplayOffset.x; + pos.y = (float)(int)pos.y + DisplayOffset.y; + float x = pos.x; + float y = pos.y; + if (y > clip_rect.w) + return; + + const float scale = size / FontSize; + const float line_height = FontSize * scale; + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + // Skip non-visible lines + const char* s = text_begin; + if (!word_wrap_enabled && y + line_height < clip_rect.y) + while (s < text_end && *s != '\n') // Fast-forward to next line + s++; + + // Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) + const int vtx_count_max = (int)(text_end - s) * 4; + const int idx_count_max = (int)(text_end - s) * 6; + const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; + draw_list->PrimReserve(idx_count_max, vtx_count_max); + + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + unsigned int vtx_current_idx = draw_list->_VtxCurrentIdx; + + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - pos.x)); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + x = pos.x; + y += line_height; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (ImCharIsSpace(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source + unsigned int c = (unsigned int)*s; + if (c < 0x80) + { + s += 1; + } + else + { + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) // Malformed UTF-8? + break; + } + + if (c < 32) + { + if (c == '\n') + { + x = pos.x; + y += line_height; + + if (y > clip_rect.w) + break; + if (!word_wrap_enabled && y + line_height < clip_rect.y) + while (s < text_end && *s != '\n') // Fast-forward to next line + s++; + continue; + } + if (c == '\r') + continue; + } + + float char_width = 0.0f; + if (const ImFontGlyph* glyph = FindGlyph((unsigned short)c)) + { + char_width = glyph->AdvanceX * scale; + + // Arbitrarily assume that both space and tabs are empty glyphs as an optimization + if (c != ' ' && c != '\t') + { + // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w + float x1 = x + glyph->X0 * scale; + float x2 = x + glyph->X1 * scale; + float y1 = y + glyph->Y0 * scale; + float y2 = y + glyph->Y1 * scale; + if (x1 <= clip_rect.z && x2 >= clip_rect.x) + { + // Render a character + float u1 = glyph->U0; + float v1 = glyph->V0; + float u2 = glyph->U1; + float v2 = glyph->V1; + + // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. + if (cpu_fine_clip) + { + if (x1 < clip_rect.x) + { + u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); + x1 = clip_rect.x; + } + if (y1 < clip_rect.y) + { + v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); + y1 = clip_rect.y; + } + if (x2 > clip_rect.z) + { + u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); + x2 = clip_rect.z; + } + if (y2 > clip_rect.w) + { + v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); + y2 = clip_rect.w; + } + if (y1 >= y2) + { + x += char_width; + continue; + } + } + + // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + { + idx_write[0] = (ImDrawIdx)(vtx_current_idx); idx_write[1] = (ImDrawIdx)(vtx_current_idx+1); idx_write[2] = (ImDrawIdx)(vtx_current_idx+2); + idx_write[3] = (ImDrawIdx)(vtx_current_idx); idx_write[4] = (ImDrawIdx)(vtx_current_idx+2); idx_write[5] = (ImDrawIdx)(vtx_current_idx+3); + vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; + vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; + vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; + vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; + vtx_write += 4; + vtx_current_idx += 4; + idx_write += 6; + } + } + } + } + + x += char_width; + } + + // Give back unused vertices + draw_list->VtxBuffer.resize((int)(vtx_write - draw_list->VtxBuffer.Data)); + draw_list->IdxBuffer.resize((int)(idx_write - draw_list->IdxBuffer.Data)); + draw_list->CmdBuffer[draw_list->CmdBuffer.Size-1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); + draw_list->_VtxWritePtr = vtx_write; + draw_list->_IdxWritePtr = idx_write; + draw_list->_VtxCurrentIdx = (unsigned int)draw_list->VtxBuffer.Size; +} + +//----------------------------------------------------------------------------- +// Internals Drawing Helpers +//----------------------------------------------------------------------------- + +static inline float ImAcos01(float x) +{ + if (x <= 0.0f) return IM_PI * 0.5f; + if (x >= 1.0f) return 0.0f; + return acosf(x); + //return (-0.69813170079773212f * x * x - 0.87266462599716477f) * x + 1.5707963267948966f; // Cheap approximation, may be enough for what we do. +} + +// FIXME: Cleanup and move code to ImDrawList. +void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding) +{ + if (x_end_norm == x_start_norm) + return; + if (x_start_norm > x_end_norm) + ImSwap(x_start_norm, x_end_norm); + + ImVec2 p0 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_start_norm), rect.Min.y); + ImVec2 p1 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_end_norm), rect.Max.y); + if (rounding == 0.0f) + { + draw_list->AddRectFilled(p0, p1, col, 0.0f); + return; + } + + rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5f, (rect.Max.y - rect.Min.y) * 0.5f) - 1.0f, 0.0f, rounding); + const float inv_rounding = 1.0f / rounding; + const float arc0_b = ImAcos01(1.0f - (p0.x - rect.Min.x) * inv_rounding); + const float arc0_e = ImAcos01(1.0f - (p1.x - rect.Min.x) * inv_rounding); + const float x0 = ImMax(p0.x, rect.Min.x + rounding); + if (arc0_b == arc0_e) + { + draw_list->PathLineTo(ImVec2(x0, p1.y)); + draw_list->PathLineTo(ImVec2(x0, p0.y)); + } + else if (arc0_b == 0.0f && arc0_e == IM_PI*0.5f) + { + draw_list->PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6); // BL + draw_list->PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9); // TR + } + else + { + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b, 3); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e, 3); // TR + } + if (p1.x > rect.Min.x + rounding) + { + const float arc1_b = ImAcos01(1.0f - (rect.Max.x - p1.x) * inv_rounding); + const float arc1_e = ImAcos01(1.0f - (rect.Max.x - p0.x) * inv_rounding); + const float x1 = ImMin(p1.x, rect.Max.x - rounding); + if (arc1_b == arc1_e) + { + draw_list->PathLineTo(ImVec2(x1, p0.y)); + draw_list->PathLineTo(ImVec2(x1, p1.y)); + } + else if (arc1_b == 0.0f && arc1_e == IM_PI*0.5f) + { + draw_list->PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12); // TR + draw_list->PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3); // BR + } + else + { + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b, 3); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e, 3); // BR + } + } + draw_list->PathFillConvex(col); +} + +//----------------------------------------------------------------------------- +// DEFAULT FONT DATA +//----------------------------------------------------------------------------- +// Compressed with stb_compress() then converted to a C array. +// Use the program in misc/fonts/binary_to_compressed_c.cpp to create the array from a TTF file. +// Decompression from stb.h (public domain) by Sean Barrett https://github.com/nothings/stb/blob/master/stb.h +//----------------------------------------------------------------------------- + +static unsigned int stb_decompress_length(unsigned char *input) +{ + return (input[8] << 24) + (input[9] << 16) + (input[10] << 8) + input[11]; +} + +static unsigned char *stb__barrier, *stb__barrier2, *stb__barrier3, *stb__barrier4; +static unsigned char *stb__dout; +static void stb__match(unsigned char *data, unsigned int length) +{ + // INVERSE of memmove... write each byte before copying the next... + IM_ASSERT (stb__dout + length <= stb__barrier); + if (stb__dout + length > stb__barrier) { stb__dout += length; return; } + if (data < stb__barrier4) { stb__dout = stb__barrier+1; return; } + while (length--) *stb__dout++ = *data++; +} + +static void stb__lit(unsigned char *data, unsigned int length) +{ + IM_ASSERT (stb__dout + length <= stb__barrier); + if (stb__dout + length > stb__barrier) { stb__dout += length; return; } + if (data < stb__barrier2) { stb__dout = stb__barrier+1; return; } + memcpy(stb__dout, data, length); + stb__dout += length; +} + +#define stb__in2(x) ((i[x] << 8) + i[(x)+1]) +#define stb__in3(x) ((i[x] << 16) + stb__in2((x)+1)) +#define stb__in4(x) ((i[x] << 24) + stb__in3((x)+1)) + +static unsigned char *stb_decompress_token(unsigned char *i) +{ + if (*i >= 0x20) { // use fewer if's for cases that expand small + if (*i >= 0x80) stb__match(stb__dout-i[1]-1, i[0] - 0x80 + 1), i += 2; + else if (*i >= 0x40) stb__match(stb__dout-(stb__in2(0) - 0x4000 + 1), i[2]+1), i += 3; + else /* *i >= 0x20 */ stb__lit(i+1, i[0] - 0x20 + 1), i += 1 + (i[0] - 0x20 + 1); + } else { // more ifs for cases that expand large, since overhead is amortized + if (*i >= 0x18) stb__match(stb__dout-(stb__in3(0) - 0x180000 + 1), i[3]+1), i += 4; + else if (*i >= 0x10) stb__match(stb__dout-(stb__in3(0) - 0x100000 + 1), stb__in2(3)+1), i += 5; + else if (*i >= 0x08) stb__lit(i+2, stb__in2(0) - 0x0800 + 1), i += 2 + (stb__in2(0) - 0x0800 + 1); + else if (*i == 0x07) stb__lit(i+3, stb__in2(1) + 1), i += 3 + (stb__in2(1) + 1); + else if (*i == 0x06) stb__match(stb__dout-(stb__in3(1)+1), i[4]+1), i += 5; + else if (*i == 0x04) stb__match(stb__dout-(stb__in3(1)+1), stb__in2(4)+1), i += 6; + } + return i; +} + +static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, unsigned int buflen) +{ + const unsigned long ADLER_MOD = 65521; + unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; + unsigned long blocklen, i; + + blocklen = buflen % 5552; + while (buflen) { + for (i=0; i + 7 < blocklen; i += 8) { + s1 += buffer[0], s2 += s1; + s1 += buffer[1], s2 += s1; + s1 += buffer[2], s2 += s1; + s1 += buffer[3], s2 += s1; + s1 += buffer[4], s2 += s1; + s1 += buffer[5], s2 += s1; + s1 += buffer[6], s2 += s1; + s1 += buffer[7], s2 += s1; + + buffer += 8; + } + + for (; i < blocklen; ++i) + s1 += *buffer++, s2 += s1; + + s1 %= ADLER_MOD, s2 %= ADLER_MOD; + buflen -= blocklen; + blocklen = 5552; + } + return (unsigned int)(s2 << 16) + (unsigned int)s1; +} + +static unsigned int stb_decompress(unsigned char *output, unsigned char *i, unsigned int length) +{ + unsigned int olen; + if (stb__in4(0) != 0x57bC0000) return 0; + if (stb__in4(4) != 0) return 0; // error! stream is > 4GB + olen = stb_decompress_length(i); + stb__barrier2 = i; + stb__barrier3 = i+length; + stb__barrier = output + olen; + stb__barrier4 = output; + i += 16; + + stb__dout = output; + for (;;) { + unsigned char *old_i = i; + i = stb_decompress_token(i); + if (i == old_i) { + if (*i == 0x05 && i[1] == 0xfa) { + IM_ASSERT(stb__dout == output + olen); + if (stb__dout != output + olen) return 0; + if (stb_adler32(1, output, olen) != (unsigned int) stb__in4(2)) + return 0; + return olen; + } else { + IM_ASSERT(0); /* NOTREACHED */ + return 0; + } + } + IM_ASSERT(stb__dout <= output + olen); + if (stb__dout > output + olen) + return 0; + } +} + +//----------------------------------------------------------------------------- +// ProggyClean.ttf +// Copyright (c) 2004, 2005 Tristan Grimmer +// MIT license (see License.txt in http://www.upperbounds.net/download/ProggyClean.ttf.zip) +// Download and more information at http://upperbounds.net +//----------------------------------------------------------------------------- +// File: 'ProggyClean.ttf' (41208 bytes) +// Exported using binary_to_compressed_c.cpp +//----------------------------------------------------------------------------- +static const char proggy_clean_ttf_compressed_data_base85[11980+1] = + "7])#######hV0qs'/###[),##/l:$#Q6>##5[n42>c-TH`->>#/e>11NNV=Bv(*:.F?uu#(gRU.o0XGH`$vhLG1hxt9?W`#,5LsCp#-i>.r$<$6pD>Lb';9Crc6tgXmKVeU2cD4Eo3R/" + "2*>]b(MC;$jPfY.;h^`IWM9Qo#t'X#(v#Y9w0#1D$CIf;W'#pWUPXOuxXuU(H9M(1=Ke$$'5F%)]0^#0X@U.a$FBjVQTSDgEKnIS7EM9>ZY9w0#L;>>#Mx&4Mvt//L[MkA#W@lK.N'[0#7RL_&#w+F%HtG9M#XL`N&.,GM4Pg;--VsM.M0rJfLH2eTM`*oJMHRC`N" + "kfimM2J,W-jXS:)r0wK#@Fge$U>`w'N7G#$#fB#$E^$#:9:hk+eOe--6x)F7*E%?76%^GMHePW-Z5l'&GiF#$956:rS?dA#fiK:)Yr+`�j@'DbG&#^$PG.Ll+DNa&VZ>1i%h1S9u5o@YaaW$e+bROPOpxTO7Stwi1::iB1q)C_=dV26J;2,]7op$]uQr@_V7$q^%lQwtuHY]=DX,n3L#0PHDO4f9>dC@O>HBuKPpP*E,N+b3L#lpR/MrTEH.IAQk.a>D[.e;mc." + "x]Ip.PH^'/aqUO/$1WxLoW0[iLAw=4h(9.`G" + "CRUxHPeR`5Mjol(dUWxZa(>STrPkrJiWx`5U7F#.g*jrohGg`cg:lSTvEY/EV_7H4Q9[Z%cnv;JQYZ5q.l7Zeas:HOIZOB?Ggv:[7MI2k).'2($5FNP&EQ(,)" + "U]W]+fh18.vsai00);D3@4ku5P?DP8aJt+;qUM]=+b'8@;mViBKx0DE[-auGl8:PJ&Dj+M6OC]O^((##]`0i)drT;-7X`=-H3[igUnPG-NZlo.#k@h#=Ork$m>a>$-?Tm$UV(?#P6YY#" + "'/###xe7q.73rI3*pP/$1>s9)W,JrM7SN]'/4C#v$U`0#V.[0>xQsH$fEmPMgY2u7Kh(G%siIfLSoS+MK2eTM$=5,M8p`A.;_R%#u[K#$x4AG8.kK/HSB==-'Ie/QTtG?-.*^N-4B/ZM" + "_3YlQC7(p7q)&](`6_c)$/*JL(L-^(]$wIM`dPtOdGA,U3:w2M-0+WomX2u7lqM2iEumMTcsF?-aT=Z-97UEnXglEn1K-bnEO`gu" + "Ft(c%=;Am_Qs@jLooI&NX;]0#j4#F14;gl8-GQpgwhrq8'=l_f-b49'UOqkLu7-##oDY2L(te+Mch&gLYtJ,MEtJfLh'x'M=$CS-ZZ%P]8bZ>#S?YY#%Q&q'3^Fw&?D)UDNrocM3A76/" + "/oL?#h7gl85[qW/NDOk%16ij;+:1a'iNIdb-ou8.P*w,v5#EI$TWS>Pot-R*H'-SEpA:g)f+O$%%`kA#G=8RMmG1&O`>to8bC]T&$,n.LoO>29sp3dt-52U%VM#q7'DHpg+#Z9%H[Ket`e;)f#Km8&+DC$I46>#Kr]]u-[=99tts1.qb#q72g1WJO81q+eN'03'eM>&1XxY-caEnO" + "j%2n8)),?ILR5^.Ibn<-X-Mq7[a82Lq:F&#ce+S9wsCK*x`569E8ew'He]h:sI[2LM$[guka3ZRd6:t%IG:;$%YiJ:Nq=?eAw;/:nnDq0(CYcMpG)qLN4$##&J-XTt,%OVU4)S1+R-#dg0/Nn?Ku1^0f$B*P:Rowwm-`0PKjYDDM'3]d39VZHEl4,.j']Pk-M.h^&:0FACm$maq-&sgw0t7/6(^xtk%" + "LuH88Fj-ekm>GA#_>568x6(OFRl-IZp`&b,_P'$MhLbxfc$mj`,O;&%W2m`Zh:/)Uetw:aJ%]K9h:TcF]u_-Sj9,VK3M.*'&0D[Ca]J9gp8,kAW]" + "%(?A%R$f<->Zts'^kn=-^@c4%-pY6qI%J%1IGxfLU9CP8cbPlXv);C=b),<2mOvP8up,UVf3839acAWAW-W?#ao/^#%KYo8fRULNd2.>%m]UK:n%r$'sw]J;5pAoO_#2mO3n,'=H5(et" + "Hg*`+RLgv>=4U8guD$I%D:W>-r5V*%j*W:Kvej.Lp$'?;++O'>()jLR-^u68PHm8ZFWe+ej8h:9r6L*0//c&iH&R8pRbA#Kjm%upV1g:" + "a_#Ur7FuA#(tRh#.Y5K+@?3<-8m0$PEn;J:rh6?I6uG<-`wMU'ircp0LaE_OtlMb&1#6T.#FDKu#1Lw%u%+GM+X'e?YLfjM[VO0MbuFp7;>Q&#WIo)0@F%q7c#4XAXN-U&VBpqB>0ie&jhZ[?iLR@@_AvA-iQC(=ksRZRVp7`.=+NpBC%rh&3]R:8XDmE5^V8O(x<-+k?'(^](H.aREZSi,#1:[IXaZFOm<-ui#qUq2$##Ri;u75OK#(RtaW-K-F`S+cF]uN`-KMQ%rP/Xri.LRcB##=YL3BgM/3M" + "D?@f&1'BW-)Ju#bmmWCMkk&#TR`C,5d>g)F;t,4:@_l8G/5h4vUd%&%950:VXD'QdWoY-F$BtUwmfe$YqL'8(PWX(" + "P?^@Po3$##`MSs?DWBZ/S>+4%>fX,VWv/w'KD`LP5IbH;rTV>n3cEK8U#bX]l-/V+^lj3;vlMb&[5YQ8#pekX9JP3XUC72L,,?+Ni&co7ApnO*5NK,((W-i:$,kp'UDAO(G0Sq7MVjJs" + "bIu)'Z,*[>br5fX^:FPAWr-m2KgLQ_nN6'8uTGT5g)uLv:873UpTLgH+#FgpH'_o1780Ph8KmxQJ8#H72L4@768@Tm&Q" + "h4CB/5OvmA&,Q&QbUoi$a_%3M01H)4x7I^&KQVgtFnV+;[Pc>[m4k//,]1?#`VY[Jr*3&&slRfLiVZJ:]?=K3Sw=[$=uRB?3xk48@aege0jT6'N#(q%.O=?2S]u*(m<-" + "V8J'(1)G][68hW$5'q[GC&5j`TE?m'esFGNRM)j,ffZ?-qx8;->g4t*:CIP/[Qap7/9'#(1sao7w-.qNUdkJ)tCF&#B^;xGvn2r9FEPFFFcL@.iFNkTve$m%#QvQS8U@)2Z+3K:AKM5i" + "sZ88+dKQ)W6>J%CL`.d*(B`-n8D9oK-XV1q['-5k'cAZ69e;D_?$ZPP&s^+7])$*$#@QYi9,5P r+$%CE=68>K8r0=dSC%%(@p7" + ".m7jilQ02'0-VWAgTlGW'b)Tq7VT9q^*^$$.:&N@@" + "$&)WHtPm*5_rO0&e%K&#-30j(E4#'Zb.o/(Tpm$>K'f@[PvFl,hfINTNU6u'0pao7%XUp9]5.>%h`8_=VYbxuel.NTSsJfLacFu3B'lQSu/m6-Oqem8T+oE--$0a/k]uj9EwsG>%veR*" + "hv^BFpQj:K'#SJ,sB-'#](j.Lg92rTw-*n%@/;39rrJF,l#qV%OrtBeC6/,;qB3ebNW[?,Hqj2L.1NP&GjUR=1D8QaS3Up&@*9wP?+lo7b?@%'k4`p0Z$22%K3+iCZj?XJN4Nm&+YF]u" + "@-W$U%VEQ/,,>>#)D#%8cY#YZ?=,`Wdxu/ae&#" + "w6)R89tI#6@s'(6Bf7a&?S=^ZI_kS&ai`&=tE72L_D,;^R)7[$so8lKN%5/$(vdfq7+ebA#" + "u1p]ovUKW&Y%q]'>$1@-[xfn$7ZTp7mM,G,Ko7a&Gu%G[RMxJs[0MM%wci.LFDK)(%:_i2B5CsR8&9Z&#=mPEnm0f`<&c)QL5uJ#%u%lJj+D-r;BoFDoS97h5g)E#o:&S4weDF,9^Hoe`h*L+_a*NrLW-1pG_&2UdB8" + "6e%B/:=>)N4xeW.*wft-;$'58-ESqr#U`'6AQ]m&6/`Z>#S?YY#Vc;r7U2&326d=w&H####?TZ`*4?&.MK?LP8Vxg>$[QXc%QJv92.(Db*B)gb*BM9dM*hJMAo*c&#" + "b0v=Pjer]$gG&JXDf->'StvU7505l9$AFvgYRI^&<^b68?j#q9QX4SM'RO#&sL1IM.rJfLUAj221]d##DW=m83u5;'bYx,*Sl0hL(W;;$doB&O/TQ:(Z^xBdLjLV#*8U_72Lh+2Q8Cj0i:6hp&$C/:p(HK>T8Y[gHQ4`4)'$Ab(Nof%V'8hL&#SfD07&6D@M.*J:;$-rv29'M]8qMv-tLp,'886iaC=Hb*YJoKJ,(j%K=H`K.v9HggqBIiZu'QvBT.#=)0ukruV&.)3=(^1`o*Pj4<-#MJ+gLq9-##@HuZPN0]u:h7.T..G:;$/Usj(T7`Q8tT72LnYl<-qx8;-HV7Q-&Xdx%1a,hC=0u+HlsV>nuIQL-5" + "_>@kXQtMacfD.m-VAb8;IReM3$wf0''hra*so568'Ip&vRs849'MRYSp%:t:h5qSgwpEr$B>Q,;s(C#$)`svQuF$##-D,##,g68@2[T;.XSdN9Qe)rpt._K-#5wF)sP'##p#C0c%-Gb%" + "hd+<-j'Ai*x&&HMkT]C'OSl##5RG[JXaHN;d'uA#x._U;.`PU@(Z3dt4r152@:v,'R.Sj'w#0<-;kPI)FfJ&#AYJ&#//)>-k=m=*XnK$>=)72L]0I%>.G690a:$##<,);?;72#?x9+d;" + "^V'9;jY@;)br#q^YQpx:X#Te$Z^'=-=bGhLf:D6&bNwZ9-ZD#n^9HhLMr5G;']d&6'wYmTFmLq9wI>P(9mI[>kC-ekLC/R&CH+s'B;K-M6$EB%is00:" + "+A4[7xks.LrNk0&E)wILYF@2L'0Nb$+pv<(2.768/FrY&h$^3i&@+G%JT'<-,v`3;_)I9M^AE]CN?Cl2AZg+%4iTpT3$U4O]GKx'm9)b@p7YsvK3w^YR-" + "CdQ*:Ir<($u&)#(&?L9Rg3H)4fiEp^iI9O8KnTj,]H?D*r7'M;PwZ9K0E^k&-cpI;.p/6_vwoFMV<->#%Xi.LxVnrU(4&8/P+:hLSKj$#U%]49t'I:rgMi'FL@a:0Y-uA[39',(vbma*" + "hU%<-SRF`Tt:542R_VV$p@[p8DV[A,?1839FWdFTi1O*H&#(AL8[_P%.M>v^-))qOT*F5Cq0`Ye%+$B6i:7@0IXSsDiWP,##P`%/L-" + "S(qw%sf/@%#B6;/U7K]uZbi^Oc^2n%t<)'mEVE''n`WnJra$^TKvX5B>;_aSEK',(hwa0:i4G?.Bci.(X[?b*($,=-n<.Q%`(X=?+@Am*Js0&=3bh8K]mL69=Lb,OcZV/);TTm8VI;?%OtJ<(b4mq7M6:u?KRdFl*:xP?Yb.5)%w_I?7uk5JC+FS(m#i'k.'a0i)9<7b'fs'59hq$*5Uhv##pi^8+hIEBF`nvo`;'l0.^S1<-wUK2/Coh58KKhLj" + "M=SO*rfO`+qC`W-On.=AJ56>>i2@2LH6A:&5q`?9I3@@'04&p2/LVa*T-4<-i3;M9UvZd+N7>b*eIwg:CC)c<>nO&#$(>.Z-I&J(Q0Hd5Q%7Co-b`-cP)hI;*_F]u`Rb[.j8_Q/<&>uu+VsH$sM9TA%?)(vmJ80),P7E>)tjD%2L=-t#fK[%`v=Q8WlA2);Sa" + ">gXm8YB`1d@K#n]76-a$U,mF%Ul:#/'xoFM9QX-$.QN'>" + "[%$Z$uF6pA6Ki2O5:8w*vP1<-1`[G,)-m#>0`P&#eb#.3i)rtB61(o'$?X3B2Qft^ae_5tKL9MUe9b*sLEQ95C&`=G?@Mj=wh*'3E>=-<)Gt*Iw)'QG:`@I" + "wOf7&]1i'S01B+Ev/Nac#9S;=;YQpg_6U`*kVY39xK,[/6Aj7:'1Bm-_1EYfa1+o&o4hp7KN_Q(OlIo@S%;jVdn0'1h19w,WQhLI)3S#f$2(eb,jr*b;3Vw]*7NH%$c4Vs,eD9>XW8?N]o+(*pgC%/72LV-uW%iewS8W6m2rtCpo'RS1R84=@paTKt)>=%&1[)*vp'u+x,VrwN;&]kuO9JDbg=pO$J*.jVe;u'm0dr9l,<*wMK*Oe=g8lV_KEBFkO'oU]^=[-792#ok,)" + "i]lR8qQ2oA8wcRCZ^7w/Njh;?.stX?Q1>S1q4Bn$)K1<-rGdO'$Wr.Lc.CG)$/*JL4tNR/,SVO3,aUw'DJN:)Ss;wGn9A32ijw%FL+Z0Fn.U9;reSq)bmI32U==5ALuG&#Vf1398/pVo" + "1*c-(aY168o<`JsSbk-,1N;$>0:OUas(3:8Z972LSfF8eb=c-;>SPw7.6hn3m`9^Xkn(r.qS[0;T%&Qc=+STRxX'q1BNk3&*eu2;&8q$&x>Q#Q7^Tf+6<(d%ZVmj2bDi%.3L2n+4W'$P" + "iDDG)g,r%+?,$@?uou5tSe2aN_AQU*'IAO" + "URQ##V^Fv-XFbGM7Fl(N<3DhLGF%q.1rC$#:T__&Pi68%0xi_&[qFJ(77j_&JWoF.V735&T,[R*:xFR*K5>>#`bW-?4Ne_&6Ne_&6Ne_&n`kr-#GJcM6X;uM6X;uM(.a..^2TkL%oR(#" + ";u.T%fAr%4tJ8&><1=GHZ_+m9/#H1F^R#SC#*N=BA9(D?v[UiFY>>^8p,KKF.W]L29uLkLlu/+4T" + "w$)F./^n3+rlo+DB;5sIYGNk+i1t-69Jg--0pao7Sm#K)pdHW&;LuDNH@H>#/X-TI(;P>#,Gc>#0Su>#4`1?#8lC?#xL$#B.`$#F:r$#JF.%#NR@%#R_R%#Vke%#Zww%#_-4^Rh%Sflr-k'MS.o?.5/sWel/wpEM0%3'/1)K^f1-d>G21&v(35>V`39V7A4=onx4" + "A1OY5EI0;6Ibgr6M$HS7Q<)58C5w,;WoA*#[%T*#`1g*#d=#+#hI5+#lUG+#pbY+#tnl+#x$),#&1;,#*=M,#.I`,#2Ur,#6b.-#;w[H#iQtA#m^0B#qjBB#uvTB##-hB#'9$C#+E6C#" + "/QHC#3^ZC#7jmC#;v)D#?,)4kMYD4lVu`4m`:&5niUA5@(A5BA1]PBB:xlBCC=2CDLXMCEUtiCf&0g2'tN?PGT4CPGT4CPGT4CPGT4CPGT4CPGT4CPGT4CP" + "GT4CPGT4CPGT4CPGT4CPGT4CPGT4CP-qekC`.9kEg^+F$kwViFJTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5o,^<-28ZI'O?;xp" + "O?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xp;7q-#lLYI:xvD=#"; + +static const char* GetDefaultCompressedFontDataTTFBase85() +{ + return proggy_clean_ttf_compressed_data_base85; +} diff --git a/attachments/simple_engine/imgui/imgui_internal.h b/attachments/simple_engine/imgui/imgui_internal.h new file mode 100644 index 00000000..221e5d46 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui_internal.h @@ -0,0 +1,1157 @@ +// dear imgui, v1.60 WIP +// (internals) + +// You may use this file to debug, understand or extend ImGui features but we don't provide any guarantee of forward compatibility! +// Set: +// #define IMGUI_DEFINE_MATH_OPERATORS +// To implement maths operators for ImVec2 (disabled by default to not collide with using IM_VEC2_CLASS_EXTRA along with your own math types+operators) + +#pragma once + +#ifndef IMGUI_VERSION +#error Must include imgui.h before imgui_internal.h +#endif + +#include // FILE* +#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf +#include // INT_MIN, INT_MAX + +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" // for stb_textedit.h +#pragma clang diagnostic ignored "-Wmissing-prototypes" // for stb_textedit.h +#pragma clang diagnostic ignored "-Wold-style-cast" +#endif + +//----------------------------------------------------------------------------- +// Forward Declarations +//----------------------------------------------------------------------------- + +struct ImRect; +struct ImGuiColMod; +struct ImGuiStyleMod; +struct ImGuiGroupData; +struct ImGuiMenuColumns; +struct ImGuiDrawContext; +struct ImGuiTextEditState; +struct ImGuiPopupRef; +struct ImGuiWindow; +struct ImGuiWindowSettings; + +typedef int ImGuiLayoutType; // enum: horizontal or vertical // enum ImGuiLayoutType_ +typedef int ImGuiButtonFlags; // flags: for ButtonEx(), ButtonBehavior() // enum ImGuiButtonFlags_ +typedef int ImGuiItemFlags; // flags: for PushItemFlag() // enum ImGuiItemFlags_ +typedef int ImGuiItemStatusFlags; // flags: storage for DC.LastItemXXX // enum ImGuiItemStatusFlags_ +typedef int ImGuiNavHighlightFlags; // flags: for RenderNavHighlight() // enum ImGuiNavHighlightFlags_ +typedef int ImGuiNavDirSourceFlags; // flags: for GetNavInputAmount2d() // enum ImGuiNavDirSourceFlags_ +typedef int ImGuiSeparatorFlags; // flags: for Separator() - internal // enum ImGuiSeparatorFlags_ +typedef int ImGuiSliderFlags; // flags: for SliderBehavior() // enum ImGuiSliderFlags_ + +//------------------------------------------------------------------------- +// STB libraries +//------------------------------------------------------------------------- + +namespace ImGuiStb +{ + +#undef STB_TEXTEDIT_STRING +#undef STB_TEXTEDIT_CHARTYPE +#define STB_TEXTEDIT_STRING ImGuiTextEditState +#define STB_TEXTEDIT_CHARTYPE ImWchar +#define STB_TEXTEDIT_GETWIDTH_NEWLINE -1.0f +#include "stb_textedit.h" + +} // namespace ImGuiStb + +//----------------------------------------------------------------------------- +// Context +//----------------------------------------------------------------------------- + +#ifndef GImGui +extern IMGUI_API ImGuiContext* GImGui; // Current implicit ImGui context pointer +#endif + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +#define IM_PI 3.14159265358979323846f + +// Helpers: UTF-8 <> wchar +IMGUI_API int ImTextStrToUtf8(char* buf, int buf_size, const ImWchar* in_text, const ImWchar* in_text_end); // return output UTF-8 bytes count +IMGUI_API int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end); // return input UTF-8 bytes count +IMGUI_API int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL); // return input UTF-8 bytes count +IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) +IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string as UTF-8 code-points + +// Helpers: Misc +IMGUI_API ImU32 ImHash(const void* data, int data_size, ImU32 seed = 0); // Pass data_size==0 for zero-terminated strings +IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, int* out_file_size = NULL, int padding_bytes = 0); +IMGUI_API FILE* ImFileOpen(const char* filename, const char* file_open_mode); +static inline bool ImCharIsSpace(int c) { return c == ' ' || c == '\t' || c == 0x3000; } +static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } + +// Helpers: Geometry +IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); +IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); + +// Helpers: String +IMGUI_API int ImStricmp(const char* str1, const char* str2); +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); +IMGUI_API char* ImStrdup(const char* str); +IMGUI_API char* ImStrchrRange(const char* str_begin, const char* str_end, char c); +IMGUI_API int ImStrlenW(const ImWchar* str); +IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); +IMGUI_API int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) IM_FMTLIST(3); + +// Helpers: Math +// We are keeping those not leaking to the user by default, in the case the user has implicit cast operators between ImVec2 and its own types (when IM_VEC2_CLASS_EXTRA is defined) +#ifdef IMGUI_DEFINE_MATH_OPERATORS +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x*rhs, lhs.y*rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x/rhs, lhs.y/rhs); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x+rhs.x, lhs.y+rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x-rhs.x, lhs.y-rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x*rhs.x, lhs.y*rhs.y); } +static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x/rhs.x, lhs.y/rhs.y); } +static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } +static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } +static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } +static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x+rhs.x, lhs.y+rhs.y, lhs.z+rhs.z, lhs.w+rhs.w); } +static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x-rhs.x, lhs.y-rhs.y, lhs.z-rhs.z, lhs.w-rhs.w); } +static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x*rhs.x, lhs.y*rhs.y, lhs.z*rhs.z, lhs.w*rhs.w); } +#endif + +static inline int ImMin(int lhs, int rhs) { return lhs < rhs ? lhs : rhs; } +static inline int ImMax(int lhs, int rhs) { return lhs >= rhs ? lhs : rhs; } +static inline float ImMin(float lhs, float rhs) { return lhs < rhs ? lhs : rhs; } +static inline float ImMax(float lhs, float rhs) { return lhs >= rhs ? lhs : rhs; } +static inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); } +static inline int ImClamp(int v, int mn, int mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } +static inline float ImClamp(float v, float mn, float mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } +static inline ImVec2 ImClamp(const ImVec2& f, const ImVec2& mn, ImVec2 mx) { return ImVec2(ImClamp(f.x,mn.x,mx.x), ImClamp(f.y,mn.y,mx.y)); } +static inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } +static inline void ImSwap(int& a, int& b) { int tmp = a; a = b; b = tmp; } +static inline void ImSwap(float& a, float& b) { float tmp = a; a = b; b = tmp; } +static inline int ImLerp(int a, int b, float t) { return (int)(a + (b - a) * t); } +static inline float ImLerp(float a, float b, float t) { return a + (b - a) * t; } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t) { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t) { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); } +static inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t) { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); } +static inline float ImLengthSqr(const ImVec2& lhs) { return lhs.x*lhs.x + lhs.y*lhs.y; } +static inline float ImLengthSqr(const ImVec4& lhs) { return lhs.x*lhs.x + lhs.y*lhs.y + lhs.z*lhs.z + lhs.w*lhs.w; } +static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = lhs.x*lhs.x + lhs.y*lhs.y; if (d > 0.0f) return 1.0f / sqrtf(d); return fail_value; } +static inline float ImFloor(float f) { return (float)(int)f; } +static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2((float)(int)v.x, (float)(int)v.y); } +static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } +static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } +static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; } +static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } + +// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. +// Defining a custom placement new() with a dummy parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +struct ImNewPlacementDummy {}; +inline void* operator new(size_t, ImNewPlacementDummy, void* ptr) { return ptr; } +inline void operator delete(void*, ImNewPlacementDummy, void*) {} // This is only required so we can use the symetrical new() +#define IM_PLACEMENT_NEW(_PTR) new(ImNewPlacementDummy(), _PTR) +#define IM_NEW(_TYPE) new(ImNewPlacementDummy(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE +template void IM_DELETE(T*& p) { if (p) { p->~T(); ImGui::MemFree(p); p = NULL; } } + +//----------------------------------------------------------------------------- +// Types +//----------------------------------------------------------------------------- + +enum ImGuiButtonFlags_ +{ + ImGuiButtonFlags_Repeat = 1 << 0, // hold to repeat + ImGuiButtonFlags_PressedOnClickRelease = 1 << 1, // return true on click + release on same item [DEFAULT if no PressedOn* flag is set] + ImGuiButtonFlags_PressedOnClick = 1 << 2, // return true on click (default requires click+release) + ImGuiButtonFlags_PressedOnRelease = 1 << 3, // return true on release (default requires click+release) + ImGuiButtonFlags_PressedOnDoubleClick = 1 << 4, // return true on double-click (default requires click+release) + ImGuiButtonFlags_FlattenChildren = 1 << 5, // allow interactions even if a child window is overlapping + ImGuiButtonFlags_AllowItemOverlap = 1 << 6, // require previous frame HoveredId to either match id or be null before being usable, use along with SetItemAllowOverlap() + ImGuiButtonFlags_DontClosePopups = 1 << 7, // disable automatically closing parent popup on press // [UNUSED] + ImGuiButtonFlags_Disabled = 1 << 8, // disable interactions + ImGuiButtonFlags_AlignTextBaseLine = 1 << 9, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine + ImGuiButtonFlags_NoKeyModifiers = 1 << 10, // disable interaction if a key modifier is held + ImGuiButtonFlags_NoHoldingActiveID = 1 << 11, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) + ImGuiButtonFlags_PressedOnDragDropHold = 1 << 12, // press when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) + ImGuiButtonFlags_NoNavFocus = 1 << 13 // don't override navigation focus when activated +}; + +enum ImGuiSliderFlags_ +{ + ImGuiSliderFlags_Vertical = 1 << 0 +}; + +enum ImGuiColumnsFlags_ +{ + // Default: 0 + ImGuiColumnsFlags_NoBorder = 1 << 0, // Disable column dividers + ImGuiColumnsFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers + ImGuiColumnsFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns + ImGuiColumnsFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window + ImGuiColumnsFlags_GrowParentContentsSize= 1 << 4 // (WIP) Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. +}; + +enum ImGuiSelectableFlagsPrivate_ +{ + // NB: need to be in sync with last value of ImGuiSelectableFlags_ + ImGuiSelectableFlags_Menu = 1 << 3, // -> PressedOnClick + ImGuiSelectableFlags_MenuItem = 1 << 4, // -> PressedOnRelease + ImGuiSelectableFlags_Disabled = 1 << 5, + ImGuiSelectableFlags_DrawFillAvailWidth = 1 << 6 +}; + +enum ImGuiSeparatorFlags_ +{ + ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar + ImGuiSeparatorFlags_Vertical = 1 << 1 +}; + +// Storage for LastItem data +enum ImGuiItemStatusFlags_ +{ + ImGuiItemStatusFlags_HoveredRect = 1 << 0, + ImGuiItemStatusFlags_HasDisplayRect = 1 << 1 +}; + +// FIXME: this is in development, not exposed/functional as a generic feature yet. +enum ImGuiLayoutType_ +{ + ImGuiLayoutType_Vertical, + ImGuiLayoutType_Horizontal +}; + +enum ImGuiAxis +{ + ImGuiAxis_None = -1, + ImGuiAxis_X = 0, + ImGuiAxis_Y = 1 +}; + +enum ImGuiPlotType +{ + ImGuiPlotType_Lines, + ImGuiPlotType_Histogram +}; + +enum ImGuiDataType +{ + ImGuiDataType_Int, + ImGuiDataType_Float, + ImGuiDataType_Float2 +}; + +enum ImGuiDir +{ + ImGuiDir_None = -1, + ImGuiDir_Left = 0, + ImGuiDir_Right = 1, + ImGuiDir_Up = 2, + ImGuiDir_Down = 3, + ImGuiDir_Count_ +}; + +enum ImGuiInputSource +{ + ImGuiInputSource_None = 0, + ImGuiInputSource_Mouse, + ImGuiInputSource_Nav, + ImGuiInputSource_NavKeyboard, // Only used occasionally for storage, not tested/handled by most code + ImGuiInputSource_NavGamepad, // " + ImGuiInputSource_Count_, +}; + +// FIXME-NAV: Clarify/expose various repeat delay/rate +enum ImGuiInputReadMode +{ + ImGuiInputReadMode_Down, + ImGuiInputReadMode_Pressed, + ImGuiInputReadMode_Released, + ImGuiInputReadMode_Repeat, + ImGuiInputReadMode_RepeatSlow, + ImGuiInputReadMode_RepeatFast +}; + +enum ImGuiNavHighlightFlags_ +{ + ImGuiNavHighlightFlags_TypeDefault = 1 << 0, + ImGuiNavHighlightFlags_TypeThin = 1 << 1, + ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, + ImGuiNavHighlightFlags_NoRounding = 1 << 3 +}; + +enum ImGuiNavDirSourceFlags_ +{ + ImGuiNavDirSourceFlags_Keyboard = 1 << 0, + ImGuiNavDirSourceFlags_PadDPad = 1 << 1, + ImGuiNavDirSourceFlags_PadLStick = 1 << 2 +}; + +enum ImGuiNavForward +{ + ImGuiNavForward_None, + ImGuiNavForward_ForwardQueued, + ImGuiNavForward_ForwardActive +}; + +// 2D axis aligned bounding-box +// NB: we can't rely on ImVec2 math operators being available here +struct IMGUI_API ImRect +{ + ImVec2 Min; // Upper-left + ImVec2 Max; // Lower-right + + ImRect() : Min(FLT_MAX,FLT_MAX), Max(-FLT_MAX,-FLT_MAX) {} + ImRect(const ImVec2& min, const ImVec2& max) : Min(min), Max(max) {} + ImRect(const ImVec4& v) : Min(v.x, v.y), Max(v.z, v.w) {} + ImRect(float x1, float y1, float x2, float y2) : Min(x1, y1), Max(x2, y2) {} + + ImVec2 GetCenter() const { return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); } + ImVec2 GetSize() const { return ImVec2(Max.x - Min.x, Max.y - Min.y); } + float GetWidth() const { return Max.x - Min.x; } + float GetHeight() const { return Max.y - Min.y; } + ImVec2 GetTL() const { return Min; } // Top-left + ImVec2 GetTR() const { return ImVec2(Max.x, Min.y); } // Top-right + ImVec2 GetBL() const { return ImVec2(Min.x, Max.y); } // Bottom-left + ImVec2 GetBR() const { return Max; } // Bottom-right + bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } + bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } + bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } + void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } + void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } + void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } + void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } + void Translate(const ImVec2& v) { Min.x += v.x; Min.y += v.y; Max.x += v.x; Max.y += v.y; } + void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. + void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. + void Floor() { Min.x = (float)(int)Min.x; Min.y = (float)(int)Min.y; Max.x = (float)(int)Max.x; Max.y = (float)(int)Max.y; } + void FixInverted() { if (Min.x > Max.x) ImSwap(Min.x, Max.x); if (Min.y > Max.y) ImSwap(Min.y, Max.y); } + bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } + bool IsFinite() const { return Min.x != FLT_MAX; } +}; + +// Stacked color modifier, backup of modified data so we can restore it +struct ImGuiColMod +{ + ImGuiCol Col; + ImVec4 BackupValue; +}; + +// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. +struct ImGuiStyleMod +{ + ImGuiStyleVar VarIdx; + union { int BackupInt[2]; float BackupFloat[2]; }; + ImGuiStyleMod(ImGuiStyleVar idx, int v) { VarIdx = idx; BackupInt[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, float v) { VarIdx = idx; BackupFloat[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) { VarIdx = idx; BackupFloat[0] = v.x; BackupFloat[1] = v.y; } +}; + +// Stacked data for BeginGroup()/EndGroup() +struct ImGuiGroupData +{ + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + float BackupIndentX; + float BackupGroupOffsetX; + float BackupCurrentLineHeight; + float BackupCurrentLineTextBaseOffset; + float BackupLogLinePosY; + bool BackupActiveIdIsAlive; + bool AdvanceCursor; +}; + +// Simple column measurement currently used for MenuItem() only. This is very short-sighted/throw-away code and NOT a generic helper. +struct IMGUI_API ImGuiMenuColumns +{ + int Count; + float Spacing; + float Width, NextWidth; + float Pos[4], NextWidths[4]; + + ImGuiMenuColumns(); + void Update(int count, float spacing, bool clear); + float DeclColumns(float w0, float w1, float w2); + float CalcExtraSpace(float avail_w); +}; + +// Internal state of the currently focused/edited text input box +struct IMGUI_API ImGuiTextEditState +{ + ImGuiID Id; // widget id owning the text state + ImVector Text; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. + ImVector InitialText; // backup of end-user buffer at the time of focus (in UTF-8, unaltered) + ImVector TempTextBuffer; + int CurLenA, CurLenW; // we need to maintain our buffer length in both UTF-8 and wchar format. + int BufSizeA; // end-user buffer size + float ScrollX; + ImGuiStb::STB_TexteditState StbState; + float CursorAnim; + bool CursorFollow; + bool SelectedAllMouseLock; + + ImGuiTextEditState() { memset(this, 0, sizeof(*this)); } + void CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking + void CursorClamp() { StbState.cursor = ImMin(StbState.cursor, CurLenW); StbState.select_start = ImMin(StbState.select_start, CurLenW); StbState.select_end = ImMin(StbState.select_end, CurLenW); } + bool HasSelection() const { return StbState.select_start != StbState.select_end; } + void ClearSelection() { StbState.select_start = StbState.select_end = StbState.cursor; } + void SelectAll() { StbState.select_start = 0; StbState.cursor = StbState.select_end = CurLenW; StbState.has_preferred_x = false; } + void OnKeyPressed(int key); +}; + +// Data saved in imgui.ini file +struct ImGuiWindowSettings +{ + char* Name; + ImGuiID Id; + ImVec2 Pos; + ImVec2 Size; + bool Collapsed; + + ImGuiWindowSettings() { Name = NULL; Id = 0; Pos = Size = ImVec2(0,0); Collapsed = false; } +}; + +struct ImGuiSettingsHandler +{ + const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' + ImGuiID TypeHash; // == ImHash(TypeName, 0, 0) + void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); + void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); + void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); + void* UserData; + + ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } +}; + +// Storage for current popup stack +struct ImGuiPopupRef +{ + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* ParentWindow; // Set on OpenPopup() + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differenciate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup +}; + +struct ImGuiColumnData +{ + float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) + float OffsetNormBeforeResize; + ImGuiColumnsFlags Flags; // Not exposed + ImRect ClipRect; + + ImGuiColumnData() { OffsetNorm = OffsetNormBeforeResize = 0.0f; Flags = 0; } +}; + +struct ImGuiColumnsSet +{ + ImGuiID ID; + ImGuiColumnsFlags Flags; + bool IsFirstFrame; + bool IsBeingResized; + int Current; + int Count; + float MinX, MaxX; + float StartPosY; + float StartMaxPosX; // Backup of CursorMaxPos + float CellMinY, CellMaxY; + ImVector Columns; + + ImGuiColumnsSet() { Clear(); } + void Clear() + { + ID = 0; + Flags = 0; + IsFirstFrame = false; + IsBeingResized = false; + Current = 0; + Count = 1; + MinX = MaxX = 0.0f; + StartPosY = 0.0f; + StartMaxPosX = 0.0f; + CellMinY = CellMaxY = 0.0f; + Columns.clear(); + } +}; + +struct IMGUI_API ImDrawListSharedData +{ + ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas + ImFont* Font; // Current/default font (optional, for simplified AddText overload) + float FontSize; // Current/default font size (optional, for simplified AddText overload) + float CurveTessellationTol; + ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() + + // Const data + // FIXME: Bake rounded corners fill/borders in atlas + ImVec2 CircleVtx12[12]; + + ImDrawListSharedData(); +}; + +struct ImDrawDataBuilder +{ + ImVector Layers[2]; // Global layers for: regular, tooltip + + void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].resize(0); } + void ClearFreeMemory() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].clear(); } + IMGUI_API void FlattenIntoSingleLayer(); +}; + +struct ImGuiNavMoveResult +{ + ImGuiID ID; // Best candidate + ImGuiID ParentID; // Best candidate window->IDStack.back() - to compare context + ImGuiWindow* Window; // Best candidate window + float DistBox; // Best candidate box distance to current NavId + float DistCenter; // Best candidate center distance to current NavId + float DistAxial; + ImRect RectRel; // Best candidate bounding box in window relative space + + ImGuiNavMoveResult() { Clear(); } + void Clear() { ID = ParentID = 0; Window = NULL; DistBox = DistCenter = DistAxial = FLT_MAX; RectRel = ImRect(); } +}; + +// Storage for SetNexWindow** functions +struct ImGuiNextWindowData +{ + ImGuiCond PosCond; + ImGuiCond SizeCond; + ImGuiCond ContentSizeCond; + ImGuiCond CollapsedCond; + ImGuiCond SizeConstraintCond; + ImGuiCond FocusCond; + ImGuiCond BgAlphaCond; + ImVec2 PosVal; + ImVec2 PosPivotVal; + ImVec2 SizeVal; + ImVec2 ContentSizeVal; + bool CollapsedVal; + ImRect SizeConstraintRect; // Valid if 'SetNextWindowSizeConstraint' is true + ImGuiSizeCallback SizeCallback; + void* SizeCallbackUserData; + float BgAlphaVal; + + ImGuiNextWindowData() + { + PosCond = SizeCond = ContentSizeCond = CollapsedCond = SizeConstraintCond = FocusCond = BgAlphaCond = 0; + PosVal = PosPivotVal = SizeVal = ImVec2(0.0f, 0.0f); + ContentSizeVal = ImVec2(0.0f, 0.0f); + CollapsedVal = false; + SizeConstraintRect = ImRect(); + SizeCallback = NULL; + SizeCallbackUserData = NULL; + BgAlphaVal = FLT_MAX; + } + + void Clear() + { + PosCond = SizeCond = ContentSizeCond = CollapsedCond = SizeConstraintCond = FocusCond = BgAlphaCond = 0; + } +}; + +// Main state for ImGui +struct ImGuiContext +{ + bool Initialized; + bool FontAtlasOwnedByContext; // Io.Fonts-> is owned by the ImGuiContext and will be destructed along with it. + ImGuiIO IO; + ImGuiStyle Style; + ImFont* Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() + float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. + float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. + ImDrawListSharedData DrawListSharedData; + + float Time; + int FrameCount; + int FrameCountEnded; + int FrameCountRendered; + ImVector Windows; + ImVector WindowsSortBuffer; + ImVector CurrentWindowStack; + ImGuiStorage WindowsById; + int WindowsActiveCount; + ImGuiWindow* CurrentWindow; // Being drawn into + ImGuiWindow* HoveredWindow; // Will catch mouse inputs + ImGuiWindow* HoveredRootWindow; // Will catch mouse inputs (for focus/move only) + ImGuiID HoveredId; // Hovered widget + bool HoveredIdAllowOverlap; + ImGuiID HoveredIdPreviousFrame; + float HoveredIdTimer; + ImGuiID ActiveId; // Active widget + ImGuiID ActiveIdPreviousFrame; + float ActiveIdTimer; + bool ActiveIdIsAlive; // Active widget has been seen this frame + bool ActiveIdIsJustActivated; // Set at the time of activation for one frame + bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) + int ActiveIdAllowNavDirFlags; // Active widget allows using directional navigation (e.g. can activate a button and move away from it) + ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) + ImGuiWindow* ActiveIdWindow; + ImGuiInputSource ActiveIdSource; // Activating with mouse or nav (gamepad/keyboard) + ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actually window that is moved is generally MovingWindow->RootWindow. + ImVector ColorModifiers; // Stack for PushStyleColor()/PopStyleColor() + ImVector StyleModifiers; // Stack for PushStyleVar()/PopStyleVar() + ImVector FontStack; // Stack for PushFont()/PopFont() + ImVector OpenPopupStack; // Which popups are open (persistent) + ImVector CurrentPopupStack; // Which level of BeginPopup() we are in (reset every frame) + ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + bool NextTreeNodeOpenVal; // Storage for SetNextTreeNode** functions + ImGuiCond NextTreeNodeOpenCond; + + // Navigation data (for gamepad/keyboard) + ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusWindow' + ImGuiID NavId; // Focused item for navigation + ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0, also set when calling ActivateItem() + ImGuiID NavActivateDownId; // ~~ IsNavInputDown(ImGuiNavInput_Activate) ? NavId : 0 + ImGuiID NavActivatePressedId; // ~~ IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0 + ImGuiID NavInputId; // ~~ IsNavInputPressed(ImGuiNavInput_Input) ? NavId : 0 + ImGuiID NavJustTabbedId; // Just tabbed to this id. + ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame + ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest) + ImRect NavScoringRectScreen; // Rectangle used for scoring, in screen space. Based of window->DC.NavRefRectRel[], modified for directional navigation scoring. + int NavScoringCount; // Metrics for debugging + ImGuiWindow* NavWindowingTarget; // When selecting a window (holding Menu+FocusPrev/Next, or equivalent of CTRL-TAB) this window is temporarily displayed front-most. + float NavWindowingHighlightTimer; + float NavWindowingHighlightAlpha; + bool NavWindowingToggleLayer; + ImGuiInputSource NavWindowingInputSource; // Gamepad or keyboard mode + int NavLayer; // Layer we are navigating on. For now the system is hard-coded for 0=main contents and 1=menu/title bar, may expose layers later. + int NavIdTabCounter; // == NavWindow->DC.FocusIdxTabCounter at time of NavId processing + bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRefRectRel is valid + bool NavMousePosDirty; // When set we will update mouse position if (NavFlags & ImGuiNavFlags_MoveMouse) if set (NB: this not enabled by default) + bool NavDisableHighlight; // When user starts using mouse, we hide gamepad/keyboard highlight (nb: but they are still available, which is why NavDisableHighlight isn't always != NavDisableMouseHover) + bool NavDisableMouseHover; // When user starts using gamepad/keyboard, we hide mouse hovering highlight until mouse is touched again. + bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest + bool NavInitRequest; // Init request for appearing window to select first item + bool NavInitRequestFromMove; + ImGuiID NavInitResultId; + ImRect NavInitResultRectRel; + bool NavMoveFromClampedRefRect; // Set by manual scrolling, if we scroll to a point where NavId isn't visible we reset navigation from visible items + bool NavMoveRequest; // Move request for this frame + ImGuiNavForward NavMoveRequestForward; // None / ForwardQueued / ForwardActive (this is used to navigate sibling parent menus from a child menu) + ImGuiDir NavMoveDir, NavMoveDirLast; // Direction of the move request (left/right/up/down), direction of the previous move request + ImGuiNavMoveResult NavMoveResultLocal; // Best move request candidate within NavWindow + ImGuiNavMoveResult NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using the NavFlattened flag) + + // Render + ImDrawData DrawData; // Main ImDrawData instance to pass render information to the user + ImDrawDataBuilder DrawDataBuilder; + float ModalWindowDarkeningRatio; + ImDrawList OverlayDrawList; // Optional software render of mouse cursors, if io.MouseDrawCursor is set + a few debug overlays + ImGuiMouseCursor MouseCursor; + + // Drag and Drop + bool DragDropActive; + ImGuiDragDropFlags DragDropSourceFlags; + int DragDropMouseButton; + ImGuiPayload DragDropPayload; + ImRect DragDropTargetRect; + ImGuiID DragDropTargetId; + float DragDropAcceptIdCurrRectSurface; + ImGuiID DragDropAcceptIdCurr; // Target item id (set at the time of accepting the payload) + ImGuiID DragDropAcceptIdPrev; // Target item id from previous frame (we need to store this to allow for overlapping drag and drop targets) + int DragDropAcceptFrameCount; // Last time a target expressed a desire to accept the source + ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly + unsigned char DragDropPayloadBufLocal[8]; + + // Widget state + ImGuiTextEditState InputTextState; + ImFont InputTextPasswordFont; + ImGuiID ScalarAsInputTextId; // Temporary text input when CTRL+clicking on a slider, etc. + ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets + ImVec4 ColorPickerRef; + float DragCurrentValue; // Currently dragged value, always float, not rounded by end-user precision settings + ImVec2 DragLastMouseDelta; + float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio + float DragSpeedScaleSlow; + float DragSpeedScaleFast; + ImVec2 ScrollbarClickDeltaToGrabCenter; // Distance between mouse and center of grab box, normalized in parent space. Use storage? + int TooltipOverrideCount; + ImVector PrivateClipboard; // If no custom clipboard handler is defined + ImVec2 OsImePosRequest, OsImePosSet; // Cursor position request & last passed to the OS Input Method Editor + + // Settings + bool SettingsLoaded; + float SettingsDirtyTimer; // Save .ini Settings on disk when time reaches zero + ImVector SettingsWindows; // .ini settings for ImGuiWindow + ImVector SettingsHandlers; // List of .ini settings handlers + + // Logging + bool LogEnabled; + FILE* LogFile; // If != NULL log to stdout/ file + ImGuiTextBuffer* LogClipboard; // Else log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators. + int LogStartDepth; + int LogAutoExpandMaxDepth; + + // Misc + float FramerateSecPerFrame[120]; // calculate estimate of framerate for user + int FramerateSecPerFrameIdx; + float FramerateSecPerFrameAccum; + int WantCaptureMouseNextFrame; // explicit capture via CaptureInputs() sets those flags + int WantCaptureKeyboardNextFrame; + int WantTextInputNextFrame; + char TempBuffer[1024*3+1]; // temporary text buffer + + ImGuiContext(ImFontAtlas* shared_font_atlas) : OverlayDrawList(NULL) + { + Initialized = false; + Font = NULL; + FontSize = FontBaseSize = 0.0f; + FontAtlasOwnedByContext = shared_font_atlas ? false : true; + IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); + + Time = 0.0f; + FrameCount = 0; + FrameCountEnded = FrameCountRendered = -1; + WindowsActiveCount = 0; + CurrentWindow = NULL; + HoveredWindow = NULL; + HoveredRootWindow = NULL; + HoveredId = 0; + HoveredIdAllowOverlap = false; + HoveredIdPreviousFrame = 0; + HoveredIdTimer = 0.0f; + ActiveId = 0; + ActiveIdPreviousFrame = 0; + ActiveIdTimer = 0.0f; + ActiveIdIsAlive = false; + ActiveIdIsJustActivated = false; + ActiveIdAllowOverlap = false; + ActiveIdAllowNavDirFlags = 0; + ActiveIdClickOffset = ImVec2(-1,-1); + ActiveIdWindow = NULL; + ActiveIdSource = ImGuiInputSource_None; + MovingWindow = NULL; + NextTreeNodeOpenVal = false; + NextTreeNodeOpenCond = 0; + + NavWindow = NULL; + NavId = NavActivateId = NavActivateDownId = NavActivatePressedId = NavInputId = 0; + NavJustTabbedId = NavJustMovedToId = NavNextActivateId = 0; + NavScoringRectScreen = ImRect(); + NavScoringCount = 0; + NavWindowingTarget = NULL; + NavWindowingHighlightTimer = NavWindowingHighlightAlpha = 0.0f; + NavWindowingToggleLayer = false; + NavWindowingInputSource = ImGuiInputSource_None; + NavLayer = 0; + NavIdTabCounter = INT_MAX; + NavIdIsAlive = false; + NavMousePosDirty = false; + NavDisableHighlight = true; + NavDisableMouseHover = false; + NavAnyRequest = false; + NavInitRequest = false; + NavInitRequestFromMove = false; + NavInitResultId = 0; + NavMoveFromClampedRefRect = false; + NavMoveRequest = false; + NavMoveRequestForward = ImGuiNavForward_None; + NavMoveDir = NavMoveDirLast = ImGuiDir_None; + + ModalWindowDarkeningRatio = 0.0f; + OverlayDrawList._Data = &DrawListSharedData; + OverlayDrawList._OwnerName = "##Overlay"; // Give it a name for debugging + MouseCursor = ImGuiMouseCursor_Arrow; + + DragDropActive = false; + DragDropSourceFlags = 0; + DragDropMouseButton = -1; + DragDropTargetId = 0; + DragDropAcceptIdCurrRectSurface = 0.0f; + DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; + DragDropAcceptFrameCount = -1; + memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); + + ScalarAsInputTextId = 0; + ColorEditOptions = ImGuiColorEditFlags__OptionsDefault; + DragCurrentValue = 0.0f; + DragLastMouseDelta = ImVec2(0.0f, 0.0f); + DragSpeedDefaultRatio = 1.0f / 100.0f; + DragSpeedScaleSlow = 1.0f / 100.0f; + DragSpeedScaleFast = 10.0f; + ScrollbarClickDeltaToGrabCenter = ImVec2(0.0f, 0.0f); + TooltipOverrideCount = 0; + OsImePosRequest = OsImePosSet = ImVec2(-1.0f, -1.0f); + + SettingsLoaded = false; + SettingsDirtyTimer = 0.0f; + + LogEnabled = false; + LogFile = NULL; + LogClipboard = NULL; + LogStartDepth = 0; + LogAutoExpandMaxDepth = 2; + + memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); + FramerateSecPerFrameIdx = 0; + FramerateSecPerFrameAccum = 0.0f; + WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; + memset(TempBuffer, 0, sizeof(TempBuffer)); + } +}; + +// Transient per-window flags, reset at the beginning of the frame. For child window, inherited from parent on first Begin(). +// This is going to be exposed in imgui.h when stabilized enough. +enum ImGuiItemFlags_ +{ + ImGuiItemFlags_AllowKeyboardFocus = 1 << 0, // true + ImGuiItemFlags_ButtonRepeat = 1 << 1, // false // Button() will return true multiple times based on io.KeyRepeatDelay and io.KeyRepeatRate settings. + ImGuiItemFlags_Disabled = 1 << 2, // false // FIXME-WIP: Disable interactions but doesn't affect visuals. Should be: grey out and disable interactions with widgets that affect data + view widgets (WIP) + ImGuiItemFlags_NoNav = 1 << 3, // false + ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, // false + ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, // false // MenuItem/Selectable() automatically closes current Popup window + ImGuiItemFlags_Default_ = ImGuiItemFlags_AllowKeyboardFocus +}; + +// Transient per-window data, reset at the beginning of the frame +// FIXME: That's theory, in practice the delimitation between ImGuiWindow and ImGuiDrawContext is quite tenuous and could be reconsidered. +struct IMGUI_API ImGuiDrawContext +{ + ImVec2 CursorPos; + ImVec2 CursorPosPrevLine; + ImVec2 CursorStartPos; + ImVec2 CursorMaxPos; // Used to implicitly calculate the size of our contents, always growing during the frame. Turned into window->SizeContents at the beginning of next frame + float CurrentLineHeight; + float CurrentLineTextBaseOffset; + float PrevLineHeight; + float PrevLineTextBaseOffset; + float LogLinePosY; + int TreeDepth; + ImU32 TreeDepthMayJumpToParentOnPop; // Store a copy of !g.NavIdIsAlive for TreeDepth 0..31 + ImGuiID LastItemId; + ImGuiItemStatusFlags LastItemStatusFlags; + ImRect LastItemRect; // Interaction rect + ImRect LastItemDisplayRect; // End-user display rect (only valid if LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) + bool NavHideHighlightOneFrame; + bool NavHasScroll; // Set when scrolling can be used (ScrollMax > 0.0f) + int NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) + int NavLayerCurrentMask; // = (1 << NavLayerCurrent) used by ItemAdd prior to clipping. + int NavLayerActiveMask; // Which layer have been written to (result from previous frame) + int NavLayerActiveMaskNext; // Which layer have been written to (buffer for current frame) + bool MenuBarAppending; // FIXME: Remove this + float MenuBarOffsetX; + ImVector ChildWindows; + ImGuiStorage* StateStorage; + ImGuiLayoutType LayoutType; + ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() + + // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. + ImGuiItemFlags ItemFlags; // == ItemFlagsStack.back() [empty == ImGuiItemFlags_Default] + float ItemWidth; // == ItemWidthStack.back(). 0.0: default, >0.0: width in pixels, <0.0: align xx pixels to the right of window + float TextWrapPos; // == TextWrapPosStack.back() [empty == -1.0f] + ImVectorItemFlagsStack; + ImVector ItemWidthStack; + ImVector TextWrapPosStack; + ImVectorGroupStack; + int StackSizesBackup[6]; // Store size of various stacks for asserting + + float IndentX; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) + float GroupOffsetX; + float ColumnsOffsetX; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. + ImGuiColumnsSet* ColumnsSet; // Current columns set + + ImGuiDrawContext() + { + CursorPos = CursorPosPrevLine = CursorStartPos = CursorMaxPos = ImVec2(0.0f, 0.0f); + CurrentLineHeight = PrevLineHeight = 0.0f; + CurrentLineTextBaseOffset = PrevLineTextBaseOffset = 0.0f; + LogLinePosY = -1.0f; + TreeDepth = 0; + TreeDepthMayJumpToParentOnPop = 0x00; + LastItemId = 0; + LastItemStatusFlags = 0; + LastItemRect = LastItemDisplayRect = ImRect(); + NavHideHighlightOneFrame = false; + NavHasScroll = false; + NavLayerActiveMask = NavLayerActiveMaskNext = 0x00; + NavLayerCurrent = 0; + NavLayerCurrentMask = 1 << 0; + MenuBarAppending = false; + MenuBarOffsetX = 0.0f; + StateStorage = NULL; + LayoutType = ParentLayoutType = ImGuiLayoutType_Vertical; + ItemWidth = 0.0f; + ItemFlags = ImGuiItemFlags_Default_; + TextWrapPos = -1.0f; + memset(StackSizesBackup, 0, sizeof(StackSizesBackup)); + + IndentX = 0.0f; + GroupOffsetX = 0.0f; + ColumnsOffsetX = 0.0f; + ColumnsSet = NULL; + } +}; + +// Windows data +struct IMGUI_API ImGuiWindow +{ + char* Name; + ImGuiID ID; // == ImHash(Name) + ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ + ImVec2 PosFloat; + ImVec2 Pos; // Position rounded-up to nearest pixel + ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) + ImVec2 SizeFull; // Size when non collapsed + ImVec2 SizeFullAtLastBegin; // Copy of SizeFull at the end of Begin. This is the reference value we'll use on the next frame to decide if we need scrollbars. + ImVec2 SizeContents; // Size of contents (== extents reach of the drawing cursor) from previous frame. Include decoration, window title, border, menu, etc. + ImVec2 SizeContentsExplicit; // Size of contents explicitly set by the user via SetNextWindowContentSize() + ImRect ContentsRegionRect; // Maximum visible content position in window coordinates. ~~ (SizeContentsExplicit ? SizeContentsExplicit : Size - ScrollbarSizes) - CursorStartPos, per axis + ImVec2 WindowPadding; // Window padding at the time of begin. + float WindowRounding; // Window rounding at the time of begin. + float WindowBorderSize; // Window border size at the time of begin. + ImGuiID MoveId; // == window->GetID("#MOVE") + ImGuiID ChildId; // Id of corresponding item in parent window (for child windows) + ImVec2 Scroll; + ImVec2 ScrollTarget; // target scroll position. stored as cursor position with scrolling canceled out, so the highest point is always 0.0f. (FLT_MAX for no change) + ImVec2 ScrollTargetCenterRatio; // 0.0f = scroll so that target position is at top, 0.5f = scroll so that target position is centered + bool ScrollbarX, ScrollbarY; + ImVec2 ScrollbarSizes; + bool Active; // Set to true on Begin(), unless Collapsed + bool WasActive; + bool WriteAccessed; // Set to true when any widget access the current window + bool Collapsed; // Set when collapsing window to become only title-bar + bool CollapseToggleWanted; + bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) + bool Appearing; // Set during the frame where the window is appearing (or re-appearing) + bool CloseButton; // Set when the window has a close button (p_open != NULL) + int BeginOrderWithinParent; // Order within immediate parent window, if we are a child window. Otherwise 0. + int BeginOrderWithinContext; // Order within entire imgui context. This is mostly used for debugging submission order related issues. + int BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) + ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) + int AutoFitFramesX, AutoFitFramesY; + bool AutoFitOnlyGrows; + int AutoFitChildAxises; + ImGuiDir AutoPosLastDirection; + int HiddenFrames; + ImGuiCond SetWindowPosAllowFlags; // store condition flags for next SetWindowPos() call. + ImGuiCond SetWindowSizeAllowFlags; // store condition flags for next SetWindowSize() call. + ImGuiCond SetWindowCollapsedAllowFlags; // store condition flags for next SetWindowCollapsed() call. + ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) + ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0,0) when positioning from top-left corner; ImVec2(0.5f,0.5f) for centering; ImVec2(1,1) for bottom right. + + ImGuiDrawContext DC; // Temporary per-window data, reset at the beginning of the frame + ImVector IDStack; // ID stack. ID are hashes seeded with the value at the top of the stack + ImRect ClipRect; // = DrawList->clip_rect_stack.back(). Scissoring / clipping rectangle. x1, y1, x2, y2. + ImRect WindowRectClipped; // = WindowRect just after setup in Begin(). == window->Rect() for root window. + ImRect InnerRect; + int LastFrameActive; + float ItemWidthDefault; + ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items + ImGuiStorage StateStorage; + ImVector ColumnsStorage; + float FontWindowScale; // Scale multiplier per-window + ImDrawList* DrawList; + ImGuiWindow* ParentWindow; // If we are a child _or_ popup window, this is pointing to our parent. Otherwise NULL. + ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. + ImGuiWindow* RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. + ImGuiWindow* RootWindowForTabbing; // Point to ourself or first ancestor which can be CTRL-Tabbed into. + ImGuiWindow* RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. + + ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) + ImGuiID NavLastIds[2]; // Last known NavId for this window, per layer (0/1) + ImRect NavRectRel[2]; // Reference rectangle, in window relative space + + // Navigation / Focus + // FIXME-NAV: Merge all this with the new Nav system, at least the request variables should be moved to ImGuiContext + int FocusIdxAllCounter; // Start at -1 and increase as assigned via FocusItemRegister() + int FocusIdxTabCounter; // (same, but only count widgets which you can Tab through) + int FocusIdxAllRequestCurrent; // Item being requested for focus + int FocusIdxTabRequestCurrent; // Tab-able item being requested for focus + int FocusIdxAllRequestNext; // Item being requested for focus, for next update (relies on layout to be stable between the frame pressing TAB and the next frame) + int FocusIdxTabRequestNext; // " + +public: + ImGuiWindow(ImGuiContext* context, const char* name); + ~ImGuiWindow(); + + ImGuiID GetID(const char* str, const char* str_end = NULL); + ImGuiID GetID(const void* ptr); + ImGuiID GetIDNoKeepAlive(const char* str, const char* str_end = NULL); + ImGuiID GetIDFromRectangle(const ImRect& r_abs); + + // We don't use g.FontSize because the window may be != g.CurrentWidow. + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x+Size.x, Pos.y+Size.y); } + float CalcFontSize() const { return GImGui->FontBaseSize * FontWindowScale; } + float TitleBarHeight() const { return (Flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : CalcFontSize() + GImGui->Style.FramePadding.y * 2.0f; } + ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight())); } + float MenuBarHeight() const { return (Flags & ImGuiWindowFlags_MenuBar) ? CalcFontSize() + GImGui->Style.FramePadding.y * 2.0f : 0.0f; } + ImRect MenuBarRect() const { float y1 = Pos.y + TitleBarHeight(); return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight()); } +}; + +// Backup and restore just enough data to be able to use IsItemHovered() on item A after another B in the same window has overwritten the data. +struct ImGuiItemHoveredDataBackup +{ + ImGuiID LastItemId; + ImGuiItemStatusFlags LastItemStatusFlags; + ImRect LastItemRect; + ImRect LastItemDisplayRect; + + ImGuiItemHoveredDataBackup() { Backup(); } + void Backup() { ImGuiWindow* window = GImGui->CurrentWindow; LastItemId = window->DC.LastItemId; LastItemStatusFlags = window->DC.LastItemStatusFlags; LastItemRect = window->DC.LastItemRect; LastItemDisplayRect = window->DC.LastItemDisplayRect; } + void Restore() const { ImGuiWindow* window = GImGui->CurrentWindow; window->DC.LastItemId = LastItemId; window->DC.LastItemStatusFlags = LastItemStatusFlags; window->DC.LastItemRect = LastItemRect; window->DC.LastItemDisplayRect = LastItemDisplayRect; } +}; + +//----------------------------------------------------------------------------- +// Internal API +// No guarantee of forward compatibility here. +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // We should always have a CurrentWindow in the stack (there is an implicit "Debug" window) + // If this ever crash because g.CurrentWindow is NULL it means that either + // - ImGui::NewFrame() has never been called, which is illegal. + // - You are calling ImGui functions after ImGui::Render() and before the next ImGui::NewFrame(), which is also illegal. + inline ImGuiWindow* GetCurrentWindowRead() { ImGuiContext& g = *GImGui; return g.CurrentWindow; } + inline ImGuiWindow* GetCurrentWindow() { ImGuiContext& g = *GImGui; g.CurrentWindow->WriteAccessed = true; return g.CurrentWindow; } + IMGUI_API ImGuiWindow* FindWindowByName(const char* name); + IMGUI_API void FocusWindow(ImGuiWindow* window); + IMGUI_API void BringWindowToFront(ImGuiWindow* window); + IMGUI_API void BringWindowToBack(ImGuiWindow* window); + IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent); + IMGUI_API bool IsWindowNavFocusable(ImGuiWindow* window); + + IMGUI_API void Initialize(ImGuiContext* context); + IMGUI_API void Shutdown(ImGuiContext* context); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). + + IMGUI_API void MarkIniSettingsDirty(); + IMGUI_API ImGuiSettingsHandler* FindSettingsHandler(const char* type_name); + IMGUI_API ImGuiWindowSettings* FindWindowSettings(ImGuiID id); + + IMGUI_API void SetActiveID(ImGuiID id, ImGuiWindow* window); + IMGUI_API ImGuiID GetActiveID(); + IMGUI_API void SetFocusID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void ClearActiveID(); + IMGUI_API void SetHoveredID(ImGuiID id); + IMGUI_API ImGuiID GetHoveredID(); + IMGUI_API void KeepAliveID(ImGuiID id); + + IMGUI_API void ItemSize(const ImVec2& size, float text_offset_y = 0.0f); + IMGUI_API void ItemSize(const ImRect& bb, float text_offset_y = 0.0f); + IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL); + IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id); + IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id, bool clip_even_when_logged); + IMGUI_API bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id, bool tab_stop = true); // Return true if focus is requested + IMGUI_API void FocusableItemUnregister(ImGuiWindow* window); + IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_x, float default_y); + IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); + IMGUI_API void PushMultiItemsWidths(int components, float width_full = 0.0f); + IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); + IMGUI_API void PopItemFlag(); + + IMGUI_API void SetCurrentFont(ImFont* font); + + IMGUI_API void OpenPopupEx(ImGuiID id); + IMGUI_API void ClosePopup(ImGuiID id); + IMGUI_API void ClosePopupsOverWindow(ImGuiWindow* ref_window); + IMGUI_API bool IsPopupOpen(ImGuiID id); + IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); + IMGUI_API void BeginTooltipEx(ImGuiWindowFlags extra_flags, bool override_previous_tooltip = true); + + IMGUI_API void NavInitWindow(ImGuiWindow* window, bool force_reinit); + IMGUI_API void NavMoveRequestCancel(); + IMGUI_API void ActivateItem(ImGuiID id); // Remotely activate a button, checkbox, tree node etc. given its unique ID. activation is queued and processed on the next frame when the item is encountered again. + + IMGUI_API float GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode); + IMGUI_API ImVec2 GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor = 0.0f, float fast_factor = 0.0f); + IMGUI_API int CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate); + + IMGUI_API void Scrollbar(ImGuiLayoutType direction); + IMGUI_API void VerticalSeparator(); // Vertical separator, for menu bars (use current line height). not exposed because it is misleading what it doesn't have an effect on regular layout. + IMGUI_API bool SplitterBehavior(ImGuiID id, const ImRect& bb, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f); + + IMGUI_API bool BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id); + IMGUI_API void ClearDragDrop(); + IMGUI_API bool IsDragDropPayloadBeingAccepted(); + + // FIXME-WIP: New Columns API + IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiColumnsFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). + IMGUI_API void EndColumns(); // close columns + IMGUI_API void PushColumnClipRect(int column_index = -1); + + // NB: All position are in absolute pixels coordinates (never using window coordinates internally) + // AVOID USING OUTSIDE OF IMGUI.CPP! NOT FOR PUBLIC CONSUMPTION. THOSE FUNCTIONS ARE A MESS. THEIR SIGNATURE AND BEHAVIOR WILL CHANGE, THEY NEED TO BE REFACTORED INTO SOMETHING DECENT. + IMGUI_API void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, bool hide_text_after_hash = true); + IMGUI_API void RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width); + IMGUI_API void RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0,0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border = true, float rounding = 0.0f); + IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); + IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, int rounding_corners_flags = ~0); + IMGUI_API void RenderTriangle(ImVec2 pos, ImGuiDir dir, float scale = 1.0f); + IMGUI_API void RenderBullet(ImVec2 pos); + IMGUI_API void RenderCheckMark(ImVec2 pos, ImU32 col, float sz); + IMGUI_API void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_TypeDefault); // Navigation highlight + IMGUI_API void RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); + IMGUI_API const char* FindRenderedTextEnd(const char* text, const char* text_end = NULL); // Find the optional ## from which we stop displaying text. + + IMGUI_API bool ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags = 0); + IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0,0), ImGuiButtonFlags flags = 0); + IMGUI_API bool CloseButton(ImGuiID id, const ImVec2& pos, float radius); + IMGUI_API bool ArrowButton(ImGuiID id, ImGuiDir dir, ImVec2 padding, ImGuiButtonFlags flags = 0); + + IMGUI_API bool SliderBehavior(const ImRect& frame_bb, ImGuiID id, float* v, float v_min, float v_max, float power, int decimal_precision, ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloatN(const char* label, float* v, int components, float v_min, float v_max, const char* display_format, float power); + IMGUI_API bool SliderIntN(const char* label, int* v, int components, int v_min, int v_max, const char* display_format); + + IMGUI_API bool DragBehavior(const ImRect& frame_bb, ImGuiID id, float* v, float v_speed, float v_min, float v_max, int decimal_precision, float power); + IMGUI_API bool DragFloatN(const char* label, float* v, int components, float v_speed, float v_min, float v_max, const char* display_format, float power); + IMGUI_API bool DragIntN(const char* label, int* v, int components, float v_speed, int v_min, int v_max, const char* display_format); + + IMGUI_API bool InputTextEx(const char* label, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloatN(const char* label, float* v, int components, int decimal_precision, ImGuiInputTextFlags extra_flags); + IMGUI_API bool InputIntN(const char* label, int* v, int components, ImGuiInputTextFlags extra_flags); + IMGUI_API bool InputScalarEx(const char* label, ImGuiDataType data_type, void* data_ptr, void* step_ptr, void* step_fast_ptr, const char* scalar_format, ImGuiInputTextFlags extra_flags); + IMGUI_API bool InputScalarAsWidgetReplacement(const ImRect& aabb, const char* label, ImGuiDataType data_type, void* data_ptr, ImGuiID id, int decimal_precision); + + IMGUI_API void ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags); + + IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL); + IMGUI_API bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0); // Consume previous SetNextTreeNodeOpened() data, if any. May return true when logging + IMGUI_API void TreePushRawID(ImGuiID id); + + IMGUI_API void PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size); + + IMGUI_API int ParseFormatPrecision(const char* fmt, int default_value); + IMGUI_API float RoundScalar(float value, int decimal_precision); + + // Shade functions + IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawVert* vert_start, ImDrawVert* vert_end, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); + IMGUI_API void ShadeVertsLinearAlphaGradientForLeftToRightText(ImDrawVert* vert_start, ImDrawVert* vert_end, float gradient_p0_x, float gradient_p1_x); + IMGUI_API void ShadeVertsLinearUV(ImDrawVert* vert_start, ImDrawVert* vert_end, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp); + +} // namespace ImGui + +// ImFontAtlas internals +IMGUI_API bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent); +IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* spc); +IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); +IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif diff --git a/attachments/simple_engine/imgui/stb_rect_pack.h b/attachments/simple_engine/imgui/stb_rect_pack.h new file mode 100644 index 00000000..fafd8897 --- /dev/null +++ b/attachments/simple_engine/imgui/stb_rect_pack.h @@ -0,0 +1,573 @@ +// stb_rect_pack.h - v0.08 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// Bugfixes / warning fixes +// Jeremy Jaussaud +// +// Version history: +// +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// This software is in the public domain. Where that dedication is not +// recognized, you are granted a perpetual, irrevocable license to copy, +// distribute, and modify this file as you see fit. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stbrp_context stbrp_context; +typedef struct stbrp_node stbrp_node; +typedef struct stbrp_rect stbrp_rect; + +#ifdef STBRP_LARGE_RECTS +typedef int stbrp_coord; +#else +typedef unsigned short stbrp_coord; +#endif + +STBRP_DEF void stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); +// Assign packed locations to rectangles. The rectangles are of type +// 'stbrp_rect' defined below, stored in the array 'rects', and there +// are 'num_rects' many of them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; +#ifndef STBRP_LARGE_RECTS + STBRP_ASSERT(width <= 0xffff && height <= 0xffff); +#endif + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; +#ifdef STBRP_LARGE_RECTS + context->extra[1].y = (1<<30); +#else + context->extra[1].y = 65535; +#endif + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *, stbrp_node *first, int x0, int width, int *pwaste) +{ + //(void)c; + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height < c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + stbrp_node *L1 = NULL, *L2 = NULL; + int count=0; + cur = context->active_head; + while (cur) { + L1 = cur; + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + L2 = cur; + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +static int rect_height_compare(const void *a, const void *b) +{ + stbrp_rect *p = (stbrp_rect *) a; + stbrp_rect *q = (stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +static int rect_width_compare(const void *a, const void *b) +{ + stbrp_rect *p = (stbrp_rect *) a; + stbrp_rect *q = (stbrp_rect *) b; + if (p->w > q->w) + return -1; + if (p->w < q->w) + return 1; + return (p->h > q->h) ? -1 : (p->h < q->h); +} + +static int rect_original_order(const void *a, const void *b) +{ + stbrp_rect *p = (stbrp_rect *) a; + stbrp_rect *q = (stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +#ifdef STBRP_LARGE_RECTS +#define STBRP__MAXVAL 0xffffffff +#else +#define STBRP__MAXVAL 0xffff +#endif + +STBRP_DEF void stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + #ifndef STBRP_LARGE_RECTS + STBRP_ASSERT(rects[i].w <= 0xffff && rects[i].h <= 0xffff); + #endif + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags + for (i=0; i < num_rects; ++i) + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); +} +#endif diff --git a/attachments/simple_engine/imgui/stb_textedit.h b/attachments/simple_engine/imgui/stb_textedit.h new file mode 100644 index 00000000..3c300325 --- /dev/null +++ b/attachments/simple_engine/imgui/stb_textedit.h @@ -0,0 +1,1317 @@ +// [ImGui] this is a slightly modified version of stb_truetype.h 1.8 +// [ImGui] - fixed some minor warnings +// [ImGui] - added STB_TEXTEDIT_MOVEWORDLEFT/STB_TEXTEDIT_MOVEWORDRIGHT custom handler (#473) + +// stb_textedit.h - v1.8 - public domain - Sean Barrett +// Development of this library was sponsored by RAD Game Tools +// +// This C header file implements the guts of a multi-line text-editing +// widget; you implement display, word-wrapping, and low-level string +// insertion/deletion, and stb_textedit will map user inputs into +// insertions & deletions, plus updates to the cursor position, +// selection state, and undo state. +// +// It is intended for use in games and other systems that need to build +// their own custom widgets and which do not have heavy text-editing +// requirements (this library is not recommended for use for editing large +// texts, as its performance does not scale and it has limited undo). +// +// Non-trivial behaviors are modelled after Windows text controls. +// +// +// LICENSE +// +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// +// DEPENDENCIES +// +// Uses the C runtime function 'memmove', which you can override +// by defining STB_TEXTEDIT_memmove before the implementation. +// Uses no other functions. Performs no runtime allocations. +// +// +// VERSION HISTORY +// +// 1.8 (2016-04-02) better keyboard handling when mouse button is down +// 1.7 (2015-09-13) change y range handling in case baseline is non-0 +// 1.6 (2015-04-15) allow STB_TEXTEDIT_memmove +// 1.5 (2014-09-10) add support for secondary keys for OS X +// 1.4 (2014-08-17) fix signed/unsigned warnings +// 1.3 (2014-06-19) fix mouse clicking to round to nearest char boundary +// 1.2 (2014-05-27) fix some RAD types that had crept into the new code +// 1.1 (2013-12-15) move-by-word (requires STB_TEXTEDIT_IS_SPACE ) +// 1.0 (2012-07-26) improve documentation, initial public release +// 0.3 (2012-02-24) bugfixes, single-line mode; insert mode +// 0.2 (2011-11-28) fixes to undo/redo +// 0.1 (2010-07-08) initial version +// +// ADDITIONAL CONTRIBUTORS +// +// Ulf Winklemann: move-by-word in 1.1 +// Fabian Giesen: secondary key inputs in 1.5 +// Martins Mozeiko: STB_TEXTEDIT_memmove +// +// Bugfixes: +// Scott Graham +// Daniel Keller +// Omar Cornut +// +// USAGE +// +// This file behaves differently depending on what symbols you define +// before including it. +// +// +// Header-file mode: +// +// If you do not define STB_TEXTEDIT_IMPLEMENTATION before including this, +// it will operate in "header file" mode. In this mode, it declares a +// single public symbol, STB_TexteditState, which encapsulates the current +// state of a text widget (except for the string, which you will store +// separately). +// +// To compile in this mode, you must define STB_TEXTEDIT_CHARTYPE to a +// primitive type that defines a single character (e.g. char, wchar_t, etc). +// +// To save space or increase undo-ability, you can optionally define the +// following things that are used by the undo system: +// +// STB_TEXTEDIT_POSITIONTYPE small int type encoding a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// If you don't define these, they are set to permissive types and +// moderate sizes. The undo system does no memory allocations, so +// it grows STB_TexteditState by the worst-case storage which is (in bytes): +// +// [4 + sizeof(STB_TEXTEDIT_POSITIONTYPE)] * STB_TEXTEDIT_UNDOSTATE_COUNT +// + sizeof(STB_TEXTEDIT_CHARTYPE) * STB_TEXTEDIT_UNDOCHAR_COUNT +// +// +// Implementation mode: +// +// If you define STB_TEXTEDIT_IMPLEMENTATION before including this, it +// will compile the implementation of the text edit widget, depending +// on a large number of symbols which must be defined before the include. +// +// The implementation is defined only as static functions. You will then +// need to provide your own APIs in the same file which will access the +// static functions. +// +// The basic concept is that you provide a "string" object which +// behaves like an array of characters. stb_textedit uses indices to +// refer to positions in the string, implicitly representing positions +// in the displayed textedit. This is true for both plain text and +// rich text; even with rich text stb_truetype interacts with your +// code as if there was an array of all the displayed characters. +// +// Symbols that must be the same in header-file and implementation mode: +// +// STB_TEXTEDIT_CHARTYPE the character type +// STB_TEXTEDIT_POSITIONTYPE small type that a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// Symbols you must define for implementation mode: +// +// STB_TEXTEDIT_STRING the type of object representing a string being edited, +// typically this is a wrapper object with other data you need +// +// STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1)) +// STB_TEXTEDIT_LAYOUTROW(&r,obj,n) returns the results of laying out a line of characters +// starting from character #n (see discussion below) +// STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character +// to the xpos of the i+1'th char for a line of characters +// starting at character #n (i.e. accounts for kerning +// with previous char) +// STB_TEXTEDIT_KEYTOTEXT(k) maps a keyboard input to an insertable character +// (return type is int, -1 means not valid to insert) +// STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 0-based +// STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize +// as manually wordwrapping for end-of-line positioning +// +// STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i +// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*) +// +// STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key +// +// STB_TEXTEDIT_K_LEFT keyboard input to move cursor left +// STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right +// STB_TEXTEDIT_K_UP keyboard input to move cursor up +// STB_TEXTEDIT_K_DOWN keyboard input to move cursor down +// STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line // e.g. HOME +// STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line // e.g. END +// STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text // e.g. ctrl-HOME +// STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text // e.g. ctrl-END +// STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor +// STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor +// STB_TEXTEDIT_K_UNDO keyboard input to perform undo +// STB_TEXTEDIT_K_REDO keyboard input to perform redo +// +// Optional: +// STB_TEXTEDIT_K_INSERT keyboard input to toggle insert mode +// STB_TEXTEDIT_IS_SPACE(ch) true if character is whitespace (e.g. 'isspace'), +// required for default WORDLEFT/WORDRIGHT handlers +// STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to +// STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to +// STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word // e.g. ctrl-LEFT +// STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word // e.g. ctrl-RIGHT +// STB_TEXTEDIT_K_LINESTART2 secondary keyboard input to move cursor to start of line +// STB_TEXTEDIT_K_LINEEND2 secondary keyboard input to move cursor to end of line +// STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text +// STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text +// +// Todo: +// STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page +// STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page +// +// Keyboard input must be encoded as a single integer value; e.g. a character code +// and some bitflags that represent shift states. to simplify the interface, SHIFT must +// be a bitflag, so we can test the shifted state of cursor movements to allow selection, +// i.e. (STB_TEXTED_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow. +// +// You can encode other things, such as CONTROL or ALT, in additional bits, and +// then test for their presence in e.g. STB_TEXTEDIT_K_WORDLEFT. For example, +// my Windows implementations add an additional CONTROL bit, and an additional KEYDOWN +// bit. Then all of the STB_TEXTEDIT_K_ values bitwise-or in the KEYDOWN bit, +// and I pass both WM_KEYDOWN and WM_CHAR events to the "key" function in the +// API below. The control keys will only match WM_KEYDOWN events because of the +// keydown bit I add, and STB_TEXTEDIT_KEYTOTEXT only tests for the KEYDOWN +// bit so it only decodes WM_CHAR events. +// +// STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed +// row of characters assuming they start on the i'th character--the width and +// the height and the number of characters consumed. This allows this library +// to traverse the entire layout incrementally. You need to compute word-wrapping +// here. +// +// Each textfield keeps its own insert mode state, which is not how normal +// applications work. To keep an app-wide insert mode, update/copy the +// "insert_mode" field of STB_TexteditState before/after calling API functions. +// +// API +// +// void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +// +// void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +// int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +// void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int key) +// +// Each of these functions potentially updates the string and updates the +// state. +// +// initialize_state: +// set the textedit state to a known good default state when initially +// constructing the textedit. +// +// click: +// call this with the mouse x,y on a mouse down; it will update the cursor +// and reset the selection start/end to the cursor point. the x,y must +// be relative to the text widget, with (0,0) being the top left. +// +// drag: +// call this with the mouse x,y on a mouse drag/up; it will update the +// cursor and the selection end point +// +// cut: +// call this to delete the current selection; returns true if there was +// one. you should FIRST copy the current selection to the system paste buffer. +// (To copy, just copy the current selection out of the string yourself.) +// +// paste: +// call this to paste text at the current cursor point or over the current +// selection if there is one. +// +// key: +// call this for keyboard inputs sent to the textfield. you can use it +// for "key down" events or for "translated" key events. if you need to +// do both (as in Win32), or distinguish Unicode characters from control +// inputs, set a high bit to distinguish the two; then you can define the +// various definitions like STB_TEXTEDIT_K_LEFT have the is-key-event bit +// set, and make STB_TEXTEDIT_KEYTOCHAR check that the is-key-event bit is +// clear. +// +// When rendering, you can read the cursor position and selection state from +// the STB_TexteditState. +// +// +// Notes: +// +// This is designed to be usable in IMGUI, so it allows for the possibility of +// running in an IMGUI that has NOT cached the multi-line layout. For this +// reason, it provides an interface that is compatible with computing the +// layout incrementally--we try to make sure we make as few passes through +// as possible. (For example, to locate the mouse pointer in the text, we +// could define functions that return the X and Y positions of characters +// and binary search Y and then X, but if we're doing dynamic layout this +// will run the layout algorithm many times, so instead we manually search +// forward in one pass. Similar logic applies to e.g. up-arrow and +// down-arrow movement.) +// +// If it's run in a widget that *has* cached the layout, then this is less +// efficient, but it's not horrible on modern computers. But you wouldn't +// want to edit million-line files with it. + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Header-file mode +//// +//// + +#ifndef INCLUDE_STB_TEXTEDIT_H +#define INCLUDE_STB_TEXTEDIT_H + +//////////////////////////////////////////////////////////////////////// +// +// STB_TexteditState +// +// Definition of STB_TexteditState which you should store +// per-textfield; it includes cursor position, selection state, +// and undo state. +// + +#ifndef STB_TEXTEDIT_UNDOSTATECOUNT +#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +#endif +#ifndef STB_TEXTEDIT_UNDOCHARCOUNT +#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#endif +#ifndef STB_TEXTEDIT_CHARTYPE +#define STB_TEXTEDIT_CHARTYPE int +#endif +#ifndef STB_TEXTEDIT_POSITIONTYPE +#define STB_TEXTEDIT_POSITIONTYPE int +#endif + +typedef struct +{ + // private data + STB_TEXTEDIT_POSITIONTYPE where; + short insert_length; + short delete_length; + short char_storage; +} StbUndoRecord; + +typedef struct +{ + // private data + StbUndoRecord undo_rec [STB_TEXTEDIT_UNDOSTATECOUNT]; + STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT]; + short undo_point, redo_point; + short undo_char_point, redo_char_point; +} StbUndoState; + +typedef struct +{ + ///////////////////// + // + // public data + // + + int cursor; + // position of the text cursor within the string + + int select_start; // selection start point + int select_end; + // selection start and end point in characters; if equal, no selection. + // note that start may be less than or greater than end (e.g. when + // dragging the mouse, start is where the initial click was, and you + // can drag in either direction) + + unsigned char insert_mode; + // each textfield keeps its own insert mode state. to keep an app-wide + // insert mode, copy this value in/out of the app state + + ///////////////////// + // + // private data + // + unsigned char cursor_at_end_of_line; // not implemented yet + unsigned char initialized; + unsigned char has_preferred_x; + unsigned char single_line; + unsigned char padding1, padding2, padding3; + float preferred_x; // this determines where the cursor up/down tries to seek to along x + StbUndoState undostate; +} STB_TexteditState; + + +//////////////////////////////////////////////////////////////////////// +// +// StbTexteditRow +// +// Result of layout query, used by stb_textedit to determine where +// the text in each row is. + +// result of layout query +typedef struct +{ + float x0,x1; // starting x location, end x location (allows for align=right, etc) + float baseline_y_delta; // position of baseline relative to previous row's baseline + float ymin,ymax; // height of row above and below baseline + int num_chars; +} StbTexteditRow; +#endif //INCLUDE_STB_TEXTEDIT_H + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Implementation mode +//// +//// + + +// implementation isn't include-guarded, since it might have indirectly +// included just the "header" portion +#ifdef STB_TEXTEDIT_IMPLEMENTATION + +#ifndef STB_TEXTEDIT_memmove +#include +#define STB_TEXTEDIT_memmove memmove +#endif + + +///////////////////////////////////////////////////////////////////////////// +// +// Mouse input handling +// + +// traverse the layout to locate the nearest character to a display position +static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) +{ + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + float base_y = 0, prev_x; + int i=0, k; + + r.x0 = r.x1 = 0; + r.ymin = r.ymax = 0; + r.num_chars = 0; + + // search rows to find one that straddles 'y' + while (i < n) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + return n; + + if (i==0 && y < base_y + r.ymin) + return 0; + + if (y < base_y + r.ymax) + break; + + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // below all text, return 'after' last character + if (i >= n) + return n; + + // check if it's before the beginning of the line + if (x < r.x0) + return i; + + // check if it's before the end of the line + if (x < r.x1) { + // search characters in row for one that straddles 'x' + k = i; + prev_x = r.x0; + for (i=0; i < r.num_chars; ++i) { + float w = STB_TEXTEDIT_GETWIDTH(str, k, i); + if (x < prev_x+w) { + if (x < prev_x+w/2) + return k+i; + else + return k+i+1; + } + prev_x += w; + } + // shouldn't happen, but if it does, fall through to end-of-line case + } + + // if the last character is a newline, return that. otherwise return 'after' the last character + if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE) + return i+r.num_chars-1; + else + return i+r.num_chars; +} + +// API click: on mouse down, move the cursor to the clicked location, and reset the selection +static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +{ + state->cursor = stb_text_locate_coord(str, x, y); + state->select_start = state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; +} + +// API drag: on mouse drag, move the cursor and selection endpoint to the clicked location +static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +{ + int p = stb_text_locate_coord(str, x, y); + if (state->select_start == state->select_end) + state->select_start = state->cursor; + state->cursor = state->select_end = p; +} + +///////////////////////////////////////////////////////////////////////////// +// +// Keyboard input handling +// + +// forward declarations +static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); + +typedef struct +{ + float x,y; // position of n'th character + float height; // height of line + int first_char, length; // first char of row, and length + int prev_first; // first char of previous row +} StbFindState; + +// find the x/y location of a character, and remember info about the previous row in +// case we get a move-up event (for page up, we'll have to rescan) +static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *str, int n, int single_line) +{ + StbTexteditRow r; + int prev_start = 0; + int z = STB_TEXTEDIT_STRINGLEN(str); + int i=0, first; + + if (n == z) { + // if it's at the end, then find the last line -- simpler than trying to + // explicitly handle this case in the regular code + if (single_line) { + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + find->y = 0; + find->first_char = 0; + find->length = z; + find->height = r.ymax - r.ymin; + find->x = r.x1; + } else { + find->y = 0; + find->x = 0; + find->height = 1; + while (i < z) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + prev_start = i; + i += r.num_chars; + } + find->first_char = i; + find->length = 0; + find->prev_first = prev_start; + } + return; + } + + // search rows to find the one that straddles character n + find->y = 0; + + for(;;) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (n < i + r.num_chars) + break; + prev_start = i; + i += r.num_chars; + find->y += r.baseline_y_delta; + } + + find->first_char = first = i; + find->length = r.num_chars; + find->height = r.ymax - r.ymin; + find->prev_first = prev_start; + + // now scan to find xpos + find->x = r.x0; + i = 0; + for (i=0; first+i < n; ++i) + find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); +} + +#define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) + +// make the selection/cursor state valid if client altered the string +static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + int n = STB_TEXTEDIT_STRINGLEN(str); + if (STB_TEXT_HAS_SELECTION(state)) { + if (state->select_start > n) state->select_start = n; + if (state->select_end > n) state->select_end = n; + // if clamping forced them to be equal, move the cursor to match + if (state->select_start == state->select_end) + state->cursor = state->select_start; + } + if (state->cursor > n) state->cursor = n; +} + +// delete characters while updating undo +static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) +{ + stb_text_makeundo_delete(str, state, where, len); + STB_TEXTEDIT_DELETECHARS(str, where, len); + state->has_preferred_x = 0; +} + +// delete the section +static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + stb_textedit_clamp(str, state); + if (STB_TEXT_HAS_SELECTION(state)) { + if (state->select_start < state->select_end) { + stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start); + state->select_end = state->cursor = state->select_start; + } else { + stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end); + state->select_start = state->cursor = state->select_end; + } + state->has_preferred_x = 0; + } +} + +// canoncialize the selection so start <= end +static void stb_textedit_sortselection(STB_TexteditState *state) +{ + if (state->select_end < state->select_start) { + int temp = state->select_end; + state->select_end = state->select_start; + state->select_start = temp; + } +} + +// move cursor to first character of selection +static void stb_textedit_move_to_first(STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_sortselection(state); + state->cursor = state->select_start; + state->select_end = state->select_start; + state->has_preferred_x = 0; + } +} + +// move cursor to last character of selection +static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_sortselection(state); + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->select_start = state->select_end; + state->has_preferred_x = 0; + } +} + +#ifdef STB_TEXTEDIT_IS_SPACE +static int is_word_boundary( STB_TEXTEDIT_STRING *_str, int _idx ) +{ + return _idx > 0 ? (STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(_str,_idx-1) ) && !STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(_str, _idx) ) ) : 1; +} + +#ifndef STB_TEXTEDIT_MOVEWORDLEFT +static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *_str, int c ) +{ + while( c >= 0 && !is_word_boundary( _str, c ) ) + --c; + + if( c < 0 ) + c = 0; + + return c; +} +#define STB_TEXTEDIT_MOVEWORDLEFT stb_textedit_move_to_word_previous +#endif + +#ifndef STB_TEXTEDIT_MOVEWORDRIGHT +static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *_str, int c ) +{ + const int len = STB_TEXTEDIT_STRINGLEN(_str); + while( c < len && !is_word_boundary( _str, c ) ) + ++c; + + if( c > len ) + c = len; + + return c; +} +#define STB_TEXTEDIT_MOVEWORDRIGHT stb_textedit_move_to_word_next +#endif + +#endif + +// update selection and cursor to match each other +static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state) +{ + if (!STB_TEXT_HAS_SELECTION(state)) + state->select_start = state->select_end = state->cursor; + else + state->cursor = state->select_end; +} + +// API cut: delete selection +static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_delete_selection(str,state); // implicity clamps + state->has_preferred_x = 0; + return 1; + } + return 0; +} + +// API paste: replace existing selection with passed-in text +static int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE const *ctext, int len) +{ + STB_TEXTEDIT_CHARTYPE *text = (STB_TEXTEDIT_CHARTYPE *) ctext; + // if there's a selection, the paste should delete it + stb_textedit_clamp(str, state); + stb_textedit_delete_selection(str,state); + // try to insert the characters + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) { + stb_text_makeundo_insert(state, state->cursor, len); + state->cursor += len; + state->has_preferred_x = 0; + return 1; + } + // remove the undo since we didn't actually insert the characters + if (state->undostate.undo_point) + --state->undostate.undo_point; + return 0; +} + +// API key: process a keyboard input +static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int key) +{ +retry: + switch (key) { + default: { + int c = STB_TEXTEDIT_KEYTOTEXT(key); + if (c > 0) { + STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c; + + // can't add newline in single-line mode + if (c == '\n' && state->single_line) + break; + + if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { + stb_text_makeundo_replace(str, state, state->cursor, 1, 1); + STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { + ++state->cursor; + state->has_preferred_x = 0; + } + } else { + stb_textedit_delete_selection(str,state); // implicity clamps + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { + stb_text_makeundo_insert(state, state->cursor, 1); + ++state->cursor; + state->has_preferred_x = 0; + } + } + } + break; + } + +#ifdef STB_TEXTEDIT_K_INSERT + case STB_TEXTEDIT_K_INSERT: + state->insert_mode = !state->insert_mode; + break; +#endif + + case STB_TEXTEDIT_K_UNDO: + stb_text_undo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_REDO: + stb_text_redo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT: + // if currently there's a selection, move cursor to start of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else + if (state->cursor > 0) + --state->cursor; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_RIGHT: + // if currently there's a selection, move cursor to end of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else + ++state->cursor; + stb_textedit_clamp(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + // move selection left + if (state->select_end > 0) + --state->select_end; + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_MOVEWORDLEFT + case STB_TEXTEDIT_K_WORDLEFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else { + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor-1); + stb_textedit_clamp( str, state ); + } + break; + + case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT: + if( !STB_TEXT_HAS_SELECTION( state ) ) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor-1); + state->select_end = state->cursor; + + stb_textedit_clamp( str, state ); + break; +#endif + +#ifdef STB_TEXTEDIT_MOVEWORDRIGHT + case STB_TEXTEDIT_K_WORDRIGHT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else { + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor+1); + stb_textedit_clamp( str, state ); + } + break; + + case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT: + if( !STB_TEXT_HAS_SELECTION( state ) ) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor+1); + state->select_end = state->cursor; + + stb_textedit_clamp( str, state ); + break; +#endif + + case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + // move selection right + ++state->select_end; + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_DOWN: + case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + StbTexteditRow row; + int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + + if (state->single_line) { + // on windows, up&down in single-line behave like left&right + key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str,state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + // now find character position down a row + if (find.length) { + float goal_x = state->has_preferred_x ? state->preferred_x : find.x; + float x; + int start = find.first_char + find.length; + state->cursor = start; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i=0; i < row.num_chars; ++i) { + float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); + #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + break; + #endif + x += dx; + if (x > goal_x) + break; + ++state->cursor; + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + } + break; + } + + case STB_TEXTEDIT_K_UP: + case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + StbTexteditRow row; + int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + + if (state->single_line) { + // on windows, up&down become left&right + key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + // can only go up if there's a previous row + if (find.prev_first != find.first_char) { + // now find character position up a row + float goal_x = state->has_preferred_x ? state->preferred_x : find.x; + float x; + state->cursor = find.prev_first; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i=0; i < row.num_chars; ++i) { + float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); + #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + break; + #endif + x += dx; + if (x > goal_x) + break; + ++state->cursor; + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + } + break; + } + + case STB_TEXTEDIT_K_DELETE: + case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else { + int n = STB_TEXTEDIT_STRINGLEN(str); + if (state->cursor < n) + stb_textedit_delete(str, state, state->cursor, 1); + } + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_BACKSPACE: + case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else { + stb_textedit_clamp(str, state); + if (state->cursor > 0) { + stb_textedit_delete(str, state, state->cursor-1, 1); + --state->cursor; + } + } + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + case STB_TEXTEDIT_K_TEXTSTART2: +#endif + case STB_TEXTEDIT_K_TEXTSTART: + state->cursor = state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + case STB_TEXTEDIT_K_TEXTEND2: +#endif + case STB_TEXTEDIT_K_TEXTEND: + state->cursor = STB_TEXTEDIT_STRINGLEN(str); + state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str); + state->has_preferred_x = 0; + break; + + +#ifdef STB_TEXTEDIT_K_LINESTART2 + case STB_TEXTEDIT_K_LINESTART2: +#endif + case STB_TEXTEDIT_K_LINESTART: { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + state->cursor = find.first_char; + state->has_preferred_x = 0; + break; + } + +#ifdef STB_TEXTEDIT_K_LINEEND2 + case STB_TEXTEDIT_K_LINEEND2: +#endif + case STB_TEXTEDIT_K_LINEEND: { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + state->has_preferred_x = 0; + state->cursor = find.first_char + find.length; + if (find.length > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) == STB_TEXTEDIT_NEWLINE) + --state->cursor; + break; + } + +#ifdef STB_TEXTEDIT_K_LINESTART2 + case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + state->cursor = state->select_end = find.first_char; + state->has_preferred_x = 0; + break; + } + +#ifdef STB_TEXTEDIT_K_LINEEND2 + case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + state->has_preferred_x = 0; + state->cursor = find.first_char + find.length; + if (find.length > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) == STB_TEXTEDIT_NEWLINE) + --state->cursor; + state->select_end = state->cursor; + break; + } + +// @TODO: +// STB_TEXTEDIT_K_PGUP - move cursor up a page +// STB_TEXTEDIT_K_PGDOWN - move cursor down a page + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// Undo processing +// +// @OPTIMIZE: the undo/redo buffer should be circular + +static void stb_textedit_flush_redo(StbUndoState *state) +{ + state->redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; + state->redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; +} + +// discard the oldest entry in the undo list +static void stb_textedit_discard_undo(StbUndoState *state) +{ + if (state->undo_point > 0) { + // if the 0th undo state has characters, clean those up + if (state->undo_rec[0].char_storage >= 0) { + int n = state->undo_rec[0].insert_length, i; + // delete n characters from all other records + state->undo_char_point = state->undo_char_point - (short) n; // vsnet05 + STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) ((size_t)state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE))); + for (i=0; i < state->undo_point; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage = state->undo_rec[i].char_storage - (short) n; // vsnet05 // @OPTIMIZE: get rid of char_storage and infer it + } + --state->undo_point; + STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) ((size_t)state->undo_point*sizeof(state->undo_rec[0]))); + } +} + +// discard the oldest entry in the redo list--it's bad if this +// ever happens, but because undo & redo have to store the actual +// characters in different cases, the redo character buffer can +// fill up even though the undo buffer didn't +static void stb_textedit_discard_redo(StbUndoState *state) +{ + int k = STB_TEXTEDIT_UNDOSTATECOUNT-1; + + if (state->redo_point <= k) { + // if the k'th undo state has characters, clean those up + if (state->undo_rec[k].char_storage >= 0) { + int n = state->undo_rec[k].insert_length, i; + // delete n characters from all other records + state->redo_char_point = state->redo_char_point + (short) n; // vsnet05 + STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((size_t)(STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE))); + for (i=state->redo_point; i < k; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage = state->undo_rec[i].char_storage + (short) n; // vsnet05 + } + ++state->redo_point; + STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point-1, state->undo_rec + state->redo_point, (size_t) ((size_t)(STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point)*sizeof(state->undo_rec[0]))); + } +} + +static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numchars) +{ + // any time we create a new undo record, we discard redo + stb_textedit_flush_redo(state); + + // if we have no free records, we have to make room, by sliding the + // existing records down + if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + stb_textedit_discard_undo(state); + + // if the characters to store won't possibly fit in the buffer, we can't undo + if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) { + state->undo_point = 0; + state->undo_char_point = 0; + return NULL; + } + + // if we don't have enough free characters in the buffer, we have to make room + while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT) + stb_textedit_discard_undo(state); + + return &state->undo_rec[state->undo_point++]; +} + +static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) +{ + StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); + if (r == NULL) + return NULL; + + r->where = pos; + r->insert_length = (short) insert_len; + r->delete_length = (short) delete_len; + + if (insert_len == 0) { + r->char_storage = -1; + return NULL; + } else { + r->char_storage = state->undo_char_point; + state->undo_char_point = state->undo_char_point + (short) insert_len; + return &state->undo_char[r->char_storage]; + } +} + +static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + StbUndoState *s = &state->undostate; + StbUndoRecord u, *r; + if (s->undo_point == 0) + return; + + // we need to do two things: apply the undo record, and create a redo record + u = s->undo_rec[s->undo_point-1]; + r = &s->undo_rec[s->redo_point-1]; + r->char_storage = -1; + + r->insert_length = u.delete_length; + r->delete_length = u.insert_length; + r->where = u.where; + + if (u.delete_length) { + // if the undo record says to delete characters, then the redo record will + // need to re-insert the characters that get deleted, so we need to store + // them. + + // there are three cases: + // there's enough room to store the characters + // characters stored for *redoing* don't leave room for redo + // characters stored for *undoing* don't leave room for redo + // if the last is true, we have to bail + + if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) { + // the undo records take up too much character space; there's no space to store the redo characters + r->insert_length = 0; + } else { + int i; + + // there's definitely room to store the characters eventually + while (s->undo_char_point + u.delete_length > s->redo_char_point) { + // there's currently not enough room, so discard a redo record + stb_textedit_discard_redo(s); + // should never happen: + if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + return; + } + r = &s->undo_rec[s->redo_point-1]; + + r->char_storage = s->redo_char_point - u.delete_length; + s->redo_char_point = s->redo_char_point - (short) u.delete_length; + + // now save the characters + for (i=0; i < u.delete_length; ++i) + s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i); + } + + // now we can carry out the deletion + STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length); + } + + // check type of recorded action: + if (u.insert_length) { + // easy case: was a deletion, so we need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length); + s->undo_char_point -= u.insert_length; + } + + state->cursor = u.where + u.insert_length; + + s->undo_point--; + s->redo_point--; +} + +static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + StbUndoState *s = &state->undostate; + StbUndoRecord *u, r; + if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + return; + + // we need to do two things: apply the redo record, and create an undo record + u = &s->undo_rec[s->undo_point]; + r = s->undo_rec[s->redo_point]; + + // we KNOW there must be room for the undo record, because the redo record + // was derived from an undo record + + u->delete_length = r.insert_length; + u->insert_length = r.delete_length; + u->where = r.where; + u->char_storage = -1; + + if (r.delete_length) { + // the redo record requires us to delete characters, so the undo record + // needs to store the characters + + if (s->undo_char_point + u->insert_length > s->redo_char_point) { + u->insert_length = 0; + u->delete_length = 0; + } else { + int i; + u->char_storage = s->undo_char_point; + s->undo_char_point = s->undo_char_point + u->insert_length; + + // now save the characters + for (i=0; i < u->insert_length; ++i) + s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i); + } + + STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length); + } + + if (r.insert_length) { + // easy case: need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length); + } + + state->cursor = r.where + r.insert_length; + + s->undo_point++; + s->redo_point++; +} + +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length) +{ + stb_text_createundo(&state->undostate, where, 0, length); +} + +static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) +{ + int i; + STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); + if (p) { + for (i=0; i < length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); + } +} + +static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) +{ + int i; + STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); + if (p) { + for (i=0; i < old_length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); + } +} + +// reset the state to default +static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_line) +{ + state->undostate.undo_point = 0; + state->undostate.undo_char_point = 0; + state->undostate.redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; + state->undostate.redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->select_end = state->select_start = 0; + state->cursor = 0; + state->has_preferred_x = 0; + state->preferred_x = 0; + state->cursor_at_end_of_line = 0; + state->initialized = 1; + state->single_line = (unsigned char) is_single_line; + state->insert_mode = 0; +} + +// API initialize +static void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +{ + stb_textedit_clear_state(state, is_single_line); +} +#endif//STB_TEXTEDIT_IMPLEMENTATION diff --git a/attachments/simple_engine/imgui/stb_truetype.h b/attachments/simple_engine/imgui/stb_truetype.h new file mode 100644 index 00000000..e6dae975 --- /dev/null +++ b/attachments/simple_engine/imgui/stb_truetype.h @@ -0,0 +1,3263 @@ +// stb_truetype.h - v1.10 - public domain +// authored from 2009-2015 by Sean Barrett / RAD Game Tools +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// +// Misc other: +// Ryan Gordon +// Simon Glass +// +// Bug/warning reports/fixes: +// "Zer" on mollyrocket (with fix) +// Cass Everitt +// stoiko (Haemimont Games) +// Brian Hook +// Walter van Niftrik +// David Gow +// David Given +// Ivan-Assen Ivanov +// Anthony Pesch +// Johan Duparc +// Hou Qiming +// Fabian "ryg" Giesen +// Martins Mozeiko +// Cap Petschulat +// Omar Cornut +// github:aloucks +// Peter LaValle +// Sergey Popov +// Giumo X. Clanjor +// Higor Euripedes +// Thomas Fields +// Derek Vinyard +// +// VERSION HISTORY +// +// 1.10 (2016-04-02) user-defined fabs(); rare memory leak; remove duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use allocation userdata properly +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// variant PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// +// Full history can be found at the end of this file. +// +// LICENSE +// +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// USAGE +// +// Include this file in whatever places neeed to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// To make the implementation private to the file that generates the implementation, +// #define STBTT_STATIC +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// Improved 3D API (more shippable): +// #include "stb_rect_pack.h" -- optional, but you really want it +// stbtt_PackBegin() +// stbtt_PackSetOversample() -- for improved quality on small fonts +// stbtt_PackFontRanges() -- pack and renders +// stbtt_PackEnd() +// stbtt_GetPackedQuad() +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- use for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetCodepointKernAdvance() +// +// Starting with version 1.06, the rasterizer was replaced with a new, +// faster and generally-more-precise rasterizer. The new rasterizer more +// accurately measures pixel coverage for anti-aliasing, except in the case +// where multiple shapes overlap, in which case it overestimates the AA pixel +// coverage. Thus, anti-aliasing of intersecting shapes may look wrong. If +// this turns out to be a problem, you can re-enable the old rasterizer with +// #define STBTT_RASTERIZER_VERSION 1 +// which will incur about a 15% speed hit. +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since they different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// ADVANCED USAGE +// +// Quality: +// +// - Use the functions with Subpixel at the end to allow your characters +// to have subpixel positioning. Since the font is anti-aliased, not +// hinted, this is very import for quality. (This is not possible with +// baked fonts.) +// +// - Kerning is now supported, and if you're supporting subpixel rendering +// then kerning is worth using to give your text a polished look. +// +// Performance: +// +// - Convert Unicode codepoints to glyph indexes and operate on the glyphs; +// if you don't do this, stb_truetype is forced to do the conversion on +// every call. +// +// - There are a lot of memory allocations. We should modify it to take +// a temp buffer and allocate from the temp buffer (without freeing), +// should help performance a lot. +// +// NOTES +// +// The system uses the raw data found in the .ttf file without changing it +// and without building auxiliary data structures. This is a bit inefficient +// on little-endian systems (the data is big-endian), but assuming you're +// caching the bitmaps or glyph shapes this shouldn't be a big deal. +// +// It appears to be very hard to programmatically determine what font a +// given file is in a general way. I provide an API for this, but I don't +// recommend it. +// +// +// SOURCE STATISTICS (based on v0.6c, 2050 LOC) +// +// Documentation & header file 520 LOC \___ 660 LOC documentation +// Sample code 140 LOC / +// Truetype parsing 620 LOC ---- 620 LOC TrueType +// Software rasterization 240 LOC \ . +// Curve tesselation 120 LOC \__ 550 LOC Bitmap creation +// Bitmap management 100 LOC / +// Baked bitmap interface 70 LOC / +// Font name matching & access 150 LOC ---- 150 +// C runtime library abstraction 60 LOC ---- 60 +// +// +// PERFORMANCE MEASUREMENTS FOR 1.06: +// +// 32-bit 64-bit +// Previous release: 8.83 s 7.68 s +// Pool allocations: 7.72 s 6.34 s +// Inline sort : 6.54 s 5.65 s +// New rasterizer : 5.63 s 5.00 s + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// SAMPLE PROGRAMS +//// +// +// Incomplete text-in-3d-api example, which draws quads properly aligned to be lossless +// +#if 0 +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +unsigned char ttf_buffer[1<<20]; +unsigned char temp_bitmap[512*512]; + +stbtt_bakedchar cdata[96]; // ASCII 32..126 is 95 glyphs +GLuint ftex; + +void my_stbtt_initfont(void) +{ + fread(ttf_buffer, 1, 1<<20, fopen("c:/windows/fonts/times.ttf", "rb")); + stbtt_BakeFontBitmap(ttf_buffer,0, 32.0, temp_bitmap,512,512, 32,96, cdata); // no guarantee this fits! + // can free ttf_buffer at this point + glGenTextures(1, &ftex); + glBindTexture(GL_TEXTURE_2D, ftex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, 512,512, 0, GL_ALPHA, GL_UNSIGNED_BYTE, temp_bitmap); + // can free temp_bitmap at this point + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +} + +void my_stbtt_print(float x, float y, char *text) +{ + // assume orthographic projection with units = screen pixels, origin at top left + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, ftex); + glBegin(GL_QUADS); + while (*text) { + if (*text >= 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program (this compiles): get a single bitmap, print as ASCII art +// +#if 0 +#include +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=2; // leave a little padding in case the character extends left + char *text = "Heljo World!"; // intentionally misspelled to show 'lj' brokenness + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// of C library functions used by stb_truetype. + +#ifdef STB_TRUETYPE_IMPLEMENTATION + // #define your own (u)stbtt_int8/16/32 before including to override this + #ifndef stbtt_uint8 + typedef unsigned char stbtt_uint8; + typedef signed char stbtt_int8; + typedef unsigned short stbtt_uint16; + typedef signed short stbtt_int16; + typedef unsigned int stbtt_uint32; + typedef signed int stbtt_int32; + #endif + + typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; + typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; + + // #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h + #ifndef STBTT_ifloor + #include + #define STBTT_ifloor(x) ((int) floor(x)) + #define STBTT_iceil(x) ((int) ceil(x)) + #endif + + #ifndef STBTT_sqrt + #include + #define STBTT_sqrt(x) sqrt(x) + #endif + + #ifndef STBTT_fabs + #include + #define STBTT_fabs(x) fabs(x) + #endif + + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h + #ifndef STBTT_malloc + #include + #define STBTT_malloc(x,u) ((void)(u),malloc(x)) + #define STBTT_free(x,u) ((void)(u),free(x)) + #endif + + #ifndef STBTT_assert + #include + #define STBTT_assert(x) assert(x) + #endif + + #ifndef STBTT_strlen + #include + #define STBTT_strlen(x) strlen(x) + #endif + + #ifndef STBTT_memcpy + #include + #define STBTT_memcpy memcpy + #define STBTT_memset memset + #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#ifndef __STB_INCLUDE_STB_TRUETYPE_H__ +#define __STB_INCLUDE_STB_TRUETYPE_H__ + +#ifdef STBTT_STATIC +#define STBTT_DEF static +#else +#define STBTT_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// TEXTURE BAKING API +// +// If you use this API, you only have to call two functions ever. +// + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; +} stbtt_bakedchar; + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long +// if return is positive, the first unused row of the bitmap +// if return is negative, returns the negative of the number of characters that fit +// if return is 0, no characters fit and no rows were used +// This uses a very crappy packing. + +typedef struct +{ + float x0,y0,s0,t0; // top-left + float x1,y1,s1,t1; // bottom-right +} stbtt_aligned_quad; + +STBTT_DEF void stbtt_GetBakedQuad(stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier +// Call GetBakedQuad with char_index = 'character - first_char', and it +// creates the quad you need to draw and advances the current position. +// +// The coordinate system used assumes y increases downwards. +// +// Characters will extend both above and below the current position; +// see discussion of "BASELINE" above. +// +// It's inefficient; you might want to c&p it and optimize it. + + + +////////////////////////////////////////////////////////////////////////////// +// +// NEW TEXTURE BAKING API +// +// This provides options for packing multiple fonts into one atlas, not +// perfectly but better than nothing. + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; + float xoff2,yoff2; +} stbtt_packedchar; + +typedef struct stbtt_pack_context stbtt_pack_context; +typedef struct stbtt_fontinfo stbtt_fontinfo; +#ifndef STB_RECT_PACK_VERSION +typedef struct stbrp_rect stbrp_rect; +#endif + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); +// Initializes a packing context stored in the passed-in stbtt_pack_context. +// Future calls using this context will pack characters into the bitmap passed +// in here: a 1-channel bitmap that is weight x height. stride_in_bytes is +// the distance from one row to the next (or 0 to mean they are packed tightly +// together). "padding" is the amount of padding to leave between each +// character (normally you want '1' for bitmaps you'll use as textures with +// bilinear filtering). +// +// Returns 0 on failure, 1 on success. + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc); +// Cleans up the packing context and frees all memory. + +#define STBTT_POINT_SIZE(x) (-(x)) + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, float font_size, + int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); +// Creates character bitmaps from the font_index'th font found in fontdata (use +// font_index=0 if you don't know what that is). It creates num_chars_in_range +// bitmaps for characters with unicode values starting at first_unicode_char_in_range +// and increasing. Data for how to render them is stored in chardata_for_range; +// pass these to stbtt_GetPackedQuad to get back renderable quads. +// +// font_size is the full height of the character from ascender to descender, +// as computed by stbtt_ScaleForPixelHeight. To use a point size as computed +// by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() +// and pass that result as 'font_size': +// ..., 20 , ... // font max minus min y is 20 pixels tall +// ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall + +typedef struct +{ + float font_size; + int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint + int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints + int num_chars; + stbtt_packedchar *chardata_for_range; // output + unsigned char h_oversample, v_oversample; // don't set these, they're used internally +} stbtt_pack_range; + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); +// Creates character bitmaps from multiple ranges of characters stored in +// ranges. This will usually create a better-packed bitmap than multiple +// calls to stbtt_PackFontRange. Note that you can call this multiple +// times within a single PackBegin/PackEnd. + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); +// Oversampling a font increases the quality by allowing higher-quality subpixel +// positioning, and is especially valuable at smaller text sizes. +// +// This function sets the amount of oversampling for all following calls to +// stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given +// pack context. The default (no oversampling) is achieved by h_oversample=1 +// and v_oversample=1. The total number of pixels required is +// h_oversample*v_oversample larger than the default; for example, 2x2 +// oversampling requires 4x the storage of 1x1. For best results, render +// oversampled textures with bilinear filtering. Look at the readme in +// stb/tests/oversample for information about oversampled fonts +// +// To use with PackFontRangesGather etc., you must set it before calls +// call to PackFontRangesGatherRects. + +STBTT_DEF void stbtt_GetPackedQuad(stbtt_packedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int align_to_integer); + +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +// Calling these functions in sequence is roughly equivalent to calling +// stbtt_PackFontRanges(). If you more control over the packing of multiple +// fonts, or if you want to pack custom data into a font texture, take a look +// at the source to of stbtt_PackFontRanges() and create a custom version +// using these functions, e.g. call GatherRects multiple times, +// building up a single array of rects, then call PackRects once, +// then call RenderIntoRects repeatedly. This may result in a +// better packing than calling PackFontRanges multiple times +// (or it may not). + +// this is an opaque structure that you shouldn't mess with which holds +// all the context needed from PackBegin to PackEnd. +struct stbtt_pack_context { + void *user_allocator_context; + void *pack_info; + int width; + int height; + int stride_in_bytes; + int padding; + unsigned int h_oversample, v_oversample; + unsigned char *pixels; + void *nodes; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// FONT LOADING +// +// + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); +// Each .ttf/.ttc file may have more than one font. Each font has a sequential +// index number starting from 0. Call this function to get the font offset for +// a given index; it returns -1 if the index is out of range. A regular .ttf +// file will only define one font and it always be at offset 0, so it will +// return '0' for index 0, and -1 for all other indices. You can just skip +// this step if you know it's that kind of font. + + +// The following structure is defined publically so you can declare one on +// the stack or as a global or etc, but you should treat it as opaque. +struct stbtt_fontinfo +{ + void * userdata; + unsigned char * data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca,head,glyf,hhea,hmtx,kern; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph +}; + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); +// Given an offset into the file that defines a font, this function builds +// the necessary cached info for the rest of the system. You must allocate +// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't +// need to do anything special to free it, because the contents are pure +// value data with no additional data structures. Returns 0 on failure. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER TO GLYPH-INDEX CONVERSIOn + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); +// If you're going to perform multiple operations on the same character +// and you want a speed-up, call this function with the character you're +// going to process, then use glyph-based functions instead of the +// codepoint-based functions. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER PROPERTIES +// + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose "height" is 'pixels' tall. +// Height is measured as the distance from the highest ascender to the lowest +// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics +// and computing: +// scale = pixels / (ascent - descent) +// so if you prefer to measure height by the ascent only, use a similar calculation. + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose EM size is mapped to +// 'pixels' tall. This is probably what traditional APIs compute, but +// I'm not positive. + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); +// ascent is the coordinate above the baseline the font extends; descent +// is the coordinate below the baseline the font extends (i.e. it is typically negative) +// lineGap is the spacing between one row's descent and the next row's ascent... +// so you should advance the vertical position by "*ascent - *descent + *lineGap" +// these are expressed in unscaled coordinates, so you must multiply by +// the scale factor for a given size + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); +// the bounding box around all possible characters + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); +// leftSideBearing is the offset from the current horizontal position to the left edge of the character +// advanceWidth is the offset from the current horizontal position to the next horizontal position +// these are expressed in unscaled coordinates + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); +// an additional amount to add to the 'advance' value between ch1 and ch2 + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); +// Gets the bounding box of the visible part of the glyph, in unscaled coordinates + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); +// as above, but takes one or more glyph indices for greater efficiency + + +////////////////////////////////////////////////////////////////////////////// +// +// GLYPH SHAPES (you probably don't need these, but they have to go before +// the bitmaps for C declaration-order reasons) +// + +#ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum { + STBTT_vmove=1, + STBTT_vline, + STBTT_vcurve + }; +#endif + +#ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) + #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x,y,cx,cy; + unsigned char type,padding; + } stbtt_vertex; +#endif + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); +// returns non-zero if nothing is drawn for this glyph + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); +// returns # of vertices and fills *vertices with the pointer to them +// these are expressed in "unscaled" coordinates +// +// The shape is a series of countours. Each one starts with +// a STBTT_moveto, then consists of a series of mixed +// STBTT_lineto and STBTT_curveto segments. A lineto +// draws a line from previous endpoint to its x,y; a curveto +// draws a quadratic bezier from previous endpoint to +// its x,y, using cx,cy as the bezier control point. + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); +// frees the data allocated above + +////////////////////////////////////////////////////////////////////////////// +// +// BITMAP RENDERING +// + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); +// frees the bitmap allocated below + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// allocates a large-enough single-channel 8bpp bitmap and renders the +// specified character/glyph at the specified scale into it, with +// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). +// *width & *height are filled out with the width & height of the bitmap, +// which is stored left-to-right, top-to-bottom. +// +// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); +// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap +// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap +// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the +// width and height and positioning info for it first. + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); +// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +// get the bbox of the bitmap centered around the glyph origin; so the +// bitmap width is ix1-ix0, height is iy1-iy0, and location to place +// the bitmap top left is (leftSideBearing*scale,iy0). +// (Note that the bitmap uses y-increases-down, but the shape uses +// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); +// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel +// shift for the character + +// the following functions are equivalent to the above functions, but operate +// on glyph indices instead of Unicode codepoints (for efficiency) +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + +// @TODO: don't expose this structure +typedef struct +{ + int w,h,stride; + unsigned char *pixels; +} stbtt__bitmap; + +// rasterize a shape with quadratic beziers into a bitmap +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into + float flatness_in_pixels, // allowable error of curve in pixels + stbtt_vertex *vertices, // array of vertices defining shape + int num_verts, // number of vertices in above array + float scale_x, float scale_y, // scale applied to input vertices + float shift_x, float shift_y, // translation applied to input vertices + int x_off, int y_off, // another translation applied to input + int invert, // if non-zero, vertically flip shape + void *userdata); // context for to STBTT_MALLOC + +////////////////////////////////////////////////////////////////////////////// +// +// Finding the right font... +// +// You should really just solve this offline, keep your own tables +// of what font is what, and don't try to get it out of the .ttf file. +// That's because getting it out of the .ttf file is really hard, because +// the names in the file can appear in many possible encodings, in many +// possible languages, and e.g. if you need a case-insensitive comparison, +// the details of that depend on the encoding & language in a complex way +// (actually underspecified in truetype, but also gigantic). +// +// But you can use the provided functions in two possible ways: +// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on +// unicode-encoded names to try to find the font you want; +// you can run this before calling stbtt_InitFont() +// +// stbtt_GetFontNameString() lets you get any of the various strings +// from the file yourself and do your own comparisons on them. +// You have to have called stbtt_InitFont() first. + + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +// returns the offset (not index) of the font that matches, or -1 if none +// if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". +// if you use any other flag, use a font name like "Arial"; this checks +// the 'macStyle' header field; i don't know if fonts set this consistently +#define STBTT_MACSTYLE_DONTCARE 0 +#define STBTT_MACSTYLE_BOLD 1 +#define STBTT_MACSTYLE_ITALIC 2 +#define STBTT_MACSTYLE_UNDERSCORE 4 +#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); +// returns 1/0 whether the first string interpreted as utf8 is identical to +// the second string interpreted as big-endian utf16... useful for strings from next func + +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); +// returns the string (which may be big-endian double byte, e.g. for unicode) +// and puts the length in bytes in *length. +// +// some of the values for the IDs are below; for more see the truetype spec: +// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html +// http://www.microsoft.com/typography/otspec/name.htm + +enum { // platformID + STBTT_PLATFORM_ID_UNICODE =0, + STBTT_PLATFORM_ID_MAC =1, + STBTT_PLATFORM_ID_ISO =2, + STBTT_PLATFORM_ID_MICROSOFT =3 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 =0, + STBTT_UNICODE_EID_UNICODE_1_1 =1, + STBTT_UNICODE_EID_ISO_10646 =2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL =0, + STBTT_MS_EID_UNICODE_BMP =1, + STBTT_MS_EID_SHIFTJIS =2, + STBTT_MS_EID_UNICODE_FULL =10 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, + STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, + STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, + STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 +}; + +enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, + STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, + STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, + STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, + STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, + STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D +}; + +enum { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, + STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, + STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, + STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , + STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , + STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, + STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 +}; + +#ifdef __cplusplus +} +#endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +#ifndef STBTT_MAX_OVERSAMPLE +#define STBTT_MAX_OVERSAMPLE 8 +#endif + +#if STBTT_MAX_OVERSAMPLE > 255 +#error "STBTT_MAX_OVERSAMPLE cannot be > 255" +#endif + +typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE-1)) == 0 ? 1 : -1]; + +#ifndef STBTT_RASTERIZER_VERSION +#define STBTT_RASTERIZER_VERSION 2 +#endif + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// on platforms that don't allow misaligned reads, if we want to allow +// truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE + +#define ttBYTE(p) (* (stbtt_uint8 *) (p)) +#define ttCHAR(p) (* (stbtt_int8 *) (p)) +#define ttFixed(p) ttLONG(p) + +#if defined(STB_TRUETYPE_BIGENDIAN) && !defined(ALLOW_UNALIGNED_TRUETYPE) + + #define ttUSHORT(p) (* (stbtt_uint16 *) (p)) + #define ttSHORT(p) (* (stbtt_int16 *) (p)) + #define ttULONG(p) (* (stbtt_uint32 *) (p)) + #define ttLONG(p) (* (stbtt_int32 *) (p)) + +#else + + static stbtt_uint16 ttUSHORT(const stbtt_uint8 *p) { return p[0]*256 + p[1]; } + static stbtt_int16 ttSHORT(const stbtt_uint8 *p) { return p[0]*256 + p[1]; } + static stbtt_uint32 ttULONG(const stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + static stbtt_int32 ttLONG(const stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + +#endif + +#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) + +static int stbtt__isfont(const stbtt_uint8 *font) +{ + // check the version number + if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF + if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 + return 0; +} + +// @OPTIMIZE: binary search +static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) +{ + stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i=0; i < num_tables; ++i) { + stbtt_uint32 loc = tabledir + 16*i; + if (stbtt_tag(data+loc+0, tag)) + return ttULONG(data+loc+8); + } + return 0; +} + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *font_collection, int index) +{ + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + stbtt_int32 n = ttLONG(font_collection+8); + if (index >= n) + return -1; + return ttULONG(font_collection+12+index*4); + } + } + return -1; +} + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data2, int fontstart) +{ + stbtt_uint8 *data = (stbtt_uint8 *) data2; + stbtt_uint32 cmap, t; + stbtt_int32 i,numTables; + + info->data = data; + info->fontstart = fontstart; + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + if (!cmap || !info->loca || !info->head || !info->glyf || !info->hhea || !info->hmtx) + return 0; + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data+t+4); + else + info->numGlyphs = 0xffff; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i=0; i < numTables; ++i) { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch(ttUSHORT(data+encoding_record)) { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + break; + case STBTT_PLATFORM_ID_UNICODE: + // Mac/iOS has these + // all the encodingIDs are unicode, so we don't bother to check it + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data+info->head + 50); + return 1; +} + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) +{ + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes-6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } else if (format == 6) { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); + return 0; + } else if (format == 2) { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); + stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) + search += rangeShift*2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) { + stbtt_uint16 end; + searchRange >>= 1; + end = ttUSHORT(data + search + searchRange*2); + if (unicode_codepoint > end) + search += searchRange*2; + --entrySelector; + } + search += 2; + + { + stbtt_uint16 offset, start; + stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); + + STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item)); + start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); + if (unicode_codepoint < start) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); + + return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); + } + } else if (format == 12 || format == 13) { + stbtt_uint32 ngroups = ttULONG(data+index_map+12); + stbtt_int32 low,high; + low = 0; high = (stbtt_int32)ngroups; + // Binary search the right group. + while (low < high) { + stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); + stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid+1; + else { + stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); + if (format == 12) + return start_glyph + unicode_codepoint-start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; +} + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) +{ + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); +} + +static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) +{ + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + int g1,g2; + + if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + + if (info->indexToLocFormat == 0) { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } else { + g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); + } + + return g1==g2 ? -1 : g1; // if length is 0, return -1 +} + +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 0; + + if (x0) *x0 = ttSHORT(info->data + g + 2); + if (y0) *y0 = ttSHORT(info->data + g + 4); + if (x1) *x1 = ttSHORT(info->data + g + 6); + if (y1) *y1 = ttSHORT(info->data + g + 8); + return 1; +} + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) +{ + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); +} + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt_int16 numberOfContours; + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; +} + +static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); + } + return num_vertices; +} + +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices=0; + int num_vertices=0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) { + stbtt_uint8 flags=0,flagcount; + stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; + stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); + + m = n + 2*numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount=0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i=0; i < n; ++i) { + if (flagcount == 0) { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } else + --flagcount; + vertices[off+i].type = flags; + } + + // now load x coordinates + x=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 2) { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } else { + if (!(flags & 16)) { + x = x + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].x = (stbtt_int16) x; + } + + // now load y coordinates + y=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 4) { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } else { + if (!(flags & 32)) { + y = y + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices=0; + sx = sy = cx = cy = scx = scy = 0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + x = (stbtt_int16) vertices[off+i].x; + y = (stbtt_int16) vertices[off+i].y; + + if (next_move == i) { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off+i+1].type & 1)) { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; + } else { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off+i+1].x; + sy = (stbtt_int32) vertices[off+i+1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } else { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours+j*2); + ++j; + } else { + if (!(flags & 1)) { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + } else if (numberOfContours == -1) { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1,0,0,1,0,0}, m, n; + + flags = ttSHORT(comp); comp+=2; + gidx = ttSHORT(comp); comp+=2; + + if (flags & 2) { // XY values + if (flags & 1) { // shorts + mtx[4] = ttSHORT(comp); comp+=2; + mtx[5] = ttSHORT(comp); comp+=2; + } else { + mtx[4] = ttCHAR(comp); comp+=1; + mtx[5] = ttCHAR(comp); comp+=1; + } + } + else { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1<<3)) { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } + + // Find transformation scales. + m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); + n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) { + stbtt_vertex* v = &comp_verts[i]; + stbtt_vertex_type x,y; + x=v->x; y=v->y; + v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + x=v->cx; y=v->cy; + v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); + if (!tmp) { + if (vertices) STBTT_free(vertices, info->userdata); + if (comp_verts) STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); + STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); + if (vertices) STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1<<5); + } + } else if (numberOfContours < 0) { + // @TODO other compound variations? + STBTT_assert(0); + } else { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; +} + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) +{ + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); + } else { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); + } +} + +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data+10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) { + m = (l + r) >> 1; + straw = ttULONG(data+18+(m*6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data+22+(m*6)); + } + return 0; +} + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); +} + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) +{ + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); +} + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) +{ + if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); + if (descent) *descent = ttSHORT(info->data+info->hhea + 6); + if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); +} + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) +{ + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); +} + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) +{ + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; +} + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; +} + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) +{ + STBTT_free(v, info->userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// antialiasing software rasterizer +// + +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0=0,y0=0,x1,y1; // =0 suppresses compiler warning + if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) { + // e.g. space character + if (ix0) *ix0 = 0; + if (iy0) *iy0 = 0; + if (ix1) *ix1 = 0; + if (iy1) *iy1 = 0; + } else { + // move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) *ix0 = STBTT_ifloor( x0 * scale_x + shift_x); + if (iy0) *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); + if (ix1) *ix1 = STBTT_iceil ( x1 * scale_x + shift_x); + if (iy1) *iy1 = STBTT_iceil (-y0 * scale_y + shift_y); + } +} + +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Rasterizer + +typedef struct stbtt__hheap_chunk +{ + struct stbtt__hheap_chunk *next; +} stbtt__hheap_chunk; + +typedef struct stbtt__hheap +{ + struct stbtt__hheap_chunk *head; + void *first_free; + int num_remaining_in_head_chunk; +} stbtt__hheap; + +static void *stbtt__hheap_alloc(stbtt__hheap *hh, size_t size, void *userdata) +{ + if (hh->first_free) { + void *p = hh->first_free; + hh->first_free = * (void **) p; + return p; + } else { + if (hh->num_remaining_in_head_chunk == 0) { + int count = (size < 32 ? 2000 : size < 128 ? 800 : 100); + stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); + if (c == NULL) + return NULL; + c->next = hh->head; + hh->head = c; + hh->num_remaining_in_head_chunk = count; + } + --hh->num_remaining_in_head_chunk; + return (char *) (hh->head) + size * hh->num_remaining_in_head_chunk; + } +} + +static void stbtt__hheap_free(stbtt__hheap *hh, void *p) +{ + *(void **) p = hh->first_free; + hh->first_free = p; +} + +static void stbtt__hheap_cleanup(stbtt__hheap *hh, void *userdata) +{ + stbtt__hheap_chunk *c = hh->head; + while (c) { + stbtt__hheap_chunk *n = c->next; + STBTT_free(c, userdata); + c = n; + } +} + +typedef struct stbtt__edge { + float x0,y0, x1,y1; + int invert; +} stbtt__edge; + + +typedef struct stbtt__active_edge +{ + struct stbtt__active_edge *next; + #if STBTT_RASTERIZER_VERSION==1 + int x,dx; + float ey; + int direction; + #elif STBTT_RASTERIZER_VERSION==2 + float fx,fdx,fdy; + float direction; + float sy; + float ey; + #else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" + #endif +} stbtt__active_edge; + +#if STBTT_RASTERIZER_VERSION == 1 +#define STBTT_FIXSHIFT 10 +#define STBTT_FIX (1 << STBTT_FIXSHIFT) +#define STBTT_FIXMASK (STBTT_FIX-1) + +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + if (!z) return z; + + // round dx down to avoid overshooting + if (dxdy < 0) + z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); + else + z->dx = STBTT_ifloor(STBTT_FIX * dxdy); + + z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount + z->x -= off_x * STBTT_FIX; + + z->ey = e->y1; + z->next = 0; + z->direction = e->invert ? 1 : -1; + return z; +} +#elif STBTT_RASTERIZER_VERSION == 2 +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + //STBTT_assert(e->y0 <= start_point); + if (!z) return z; + z->fdx = dxdy; + z->fdy = dxdy != 0.0f ? (1.0f/dxdy) : 0.0f; + z->fx = e->x0 + dxdy * (start_point - e->y0); + z->fx -= off_x; + z->direction = e->invert ? 1.0f : -1.0f; + z->sy = e->y0; + z->ey = e->y1; + z->next = 0; + return z; +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#if STBTT_RASTERIZER_VERSION == 1 +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) +{ + // non-zero winding fill + int x0=0, w=0; + + while (e) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->direction; + } else { + int x1 = e->x; w += e->direction; + // if we went to zero, we need to draw + if (w == 0) { + int i = x0 >> STBTT_FIXSHIFT; + int j = x1 >> STBTT_FIXSHIFT; + + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } +} + +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) { + STBTT_memset(scanline, 0, result->w); + for (s=0; s < vsubsample; ++s) { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for(;;) { + int changed=0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) { + if (e->y1 > scan_y) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); + if (z != NULL) { + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +#elif STBTT_RASTERIZER_VERSION == 2 + +// the edge passed in here does not cross the vertical line at x or the vertical line at x+1 +// (i.e. it has already been clipped to those) +static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edge *e, float x0, float y0, float x1, float y1) +{ + if (y0 == y1) return; + STBTT_assert(y0 < y1); + STBTT_assert(e->sy <= e->ey); + if (y0 > e->ey) return; + if (y1 < e->sy) return; + if (y0 < e->sy) { + x0 += (x1-x0) * (e->sy - y0) / (y1-y0); + y0 = e->sy; + } + if (y1 > e->ey) { + x1 += (x1-x0) * (e->ey - y1) / (y1-y0); + y1 = e->ey; + } + + if (x0 == x) + STBTT_assert(x1 <= x+1); + else if (x0 == x+1) + STBTT_assert(x1 >= x); + else if (x0 <= x) + STBTT_assert(x1 <= x); + else if (x0 >= x+1) + STBTT_assert(x1 >= x+1); + else + STBTT_assert(x1 >= x && x1 <= x+1); + + if (x0 <= x && x1 <= x) + scanline[x] += e->direction * (y1-y0); + else if (x0 >= x+1 && x1 >= x+1) + ; + else { + STBTT_assert(x0 >= x && x0 <= x+1 && x1 >= x && x1 <= x+1); + scanline[x] += e->direction * (y1-y0) * (1-((x0-x)+(x1-x))/2); // coverage = 1 - average x position + } +} + +static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) +{ + float y_bottom = y_top+1; + + while (e) { + // brute force every pixel + + // compute intersection points with top & bottom + STBTT_assert(e->ey >= y_top); + + if (e->fdx == 0) { + float x0 = e->fx; + if (x0 < len) { + if (x0 >= 0) { + stbtt__handle_clipped_edge(scanline,(int) x0,e, x0,y_top, x0,y_bottom); + stbtt__handle_clipped_edge(scanline_fill-1,(int) x0+1,e, x0,y_top, x0,y_bottom); + } else { + stbtt__handle_clipped_edge(scanline_fill-1,0,e, x0,y_top, x0,y_bottom); + } + } + } else { + float x0 = e->fx; + float dx = e->fdx; + float xb = x0 + dx; + float x_top, x_bottom; + float sy0,sy1; + float dy = e->fdy; + STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); + + // compute endpoints of line segment clipped to this scanline (if the + // line segment starts on this scanline. x0 is the intersection of the + // line with y_top, but that may be off the line segment. + if (e->sy > y_top) { + x_top = x0 + dx * (e->sy - y_top); + sy0 = e->sy; + } else { + x_top = x0; + sy0 = y_top; + } + if (e->ey < y_bottom) { + x_bottom = x0 + dx * (e->ey - y_top); + sy1 = e->ey; + } else { + x_bottom = xb; + sy1 = y_bottom; + } + + if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) { + // from here on, we don't have to range check x values + + if ((int) x_top == (int) x_bottom) { + float height; + // simple case, only spans one pixel + int x = (int) x_top; + height = sy1 - sy0; + STBTT_assert(x >= 0 && x < len); + scanline[x] += e->direction * (1-((x_top - x) + (x_bottom-x))/2) * height; + scanline_fill[x] += e->direction * height; // everything right of this pixel is filled + } else { + int x,x1,x2; + float y_crossing, step, sign, area; + // covers 2+ pixels + if (x_top > x_bottom) { + // flip scanline vertically; signed area is the same + float t; + sy0 = y_bottom - (sy0 - y_top); + sy1 = y_bottom - (sy1 - y_top); + t = sy0, sy0 = sy1, sy1 = t; + t = x_bottom, x_bottom = x_top, x_top = t; + dx = -dx; + dy = -dy; + t = x0, x0 = xb, xb = t; + } + + x1 = (int) x_top; + x2 = (int) x_bottom; + // compute intersection with y axis at x1+1 + y_crossing = (x1+1 - x0) * dy + y_top; + + sign = e->direction; + // area of the rectangle covered from y0..y_crossing + area = sign * (y_crossing-sy0); + // area of the triangle (x_top,y0), (x+1,y0), (x+1,y_crossing) + scanline[x1] += area * (1-((x_top - x1)+(x1+1-x1))/2); + + step = sign * dy; + for (x = x1+1; x < x2; ++x) { + scanline[x] += area + step/2; + area += step; + } + y_crossing += dy * (x2 - (x1+1)); + + STBTT_assert(STBTT_fabs(area) <= 1.01f); + + scanline[x2] += area + sign * (1-((x2-x2)+(x_bottom-x2))/2) * (sy1-y_crossing); + + scanline_fill[x2] += sign * (sy1-sy0); + } + } else { + // if edge goes outside of box we're drawing, we require + // clipping logic. since this does not match the intended use + // of this library, we use a different, very slow brute + // force implementation + int x; + for (x=0; x < len; ++x) { + // cases: + // + // there can be up to two intersections with the pixel. any intersection + // with left or right edges can be handled by splitting into two (or three) + // regions. intersections with top & bottom do not necessitate case-wise logic. + // + // the old way of doing this found the intersections with the left & right edges, + // then used some simple logic to produce up to three segments in sorted order + // from top-to-bottom. however, this had a problem: if an x edge was epsilon + // across the x border, then the corresponding y position might not be distinct + // from the other y segment, and it might ignored as an empty segment. to avoid + // that, we need to explicitly produce segments based on x positions. + + // rename variables to clear pairs + float y0 = y_top; + float x1 = (float) (x); + float x2 = (float) (x+1); + float x3 = xb; + float y3 = y_bottom; + float y1,y2; + + // x = e->x + e->dx * (y-y_top) + // (y-y_top) = (x - e->x) / e->dx + // y = (x - e->x) / e->dx + y_top + y1 = (x - x0) / dx + y_top; + y2 = (x+1 - x0) / dx + y_top; + + if (x0 < x1 && x3 > x2) { // three segments descending down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x1 && x0 > x2) { // three segments descending down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x1 && x3 > x1) { // two segments across x, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x3 < x1 && x0 > x1) { // two segments across x, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x2 && x3 > x2) { // two segments across x+1, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x2 && x0 > x2) { // two segments across x+1, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else { // one segment + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x3,y3); + } + } + } + } + e = e->next; + } +} + +// directly AA rasterize edges w/o supersampling +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + (void)vsubsample; + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0, i; + float scanline_data[129], *scanline, *scanline2; + + if (result->w > 64) + scanline = (float *) STBTT_malloc((result->w*2+1) * sizeof(float), userdata); + else + scanline = scanline_data; + + scanline2 = scanline + result->w; + + y = off_y; + e[n].y0 = (float) (off_y + result->h) + 1; + + while (j < result->h) { + // find center of pixel for this scanline + float scan_y_top = y + 0.0f; + float scan_y_bottom = y + 1.0f; + stbtt__active_edge **step = &active; + + STBTT_memset(scanline , 0, result->w*sizeof(scanline[0])); + STBTT_memset(scanline2, 0, (result->w+1)*sizeof(scanline[0])); + + // update all active edges; + // remove all active edges that terminate before the top of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y_top) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + step = &((*step)->next); // advance through list + } + } + + // insert all edges that start before the bottom of this scanline + while (e->y0 <= scan_y_bottom) { + if (e->y0 != e->y1) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); + if (z != NULL) { + STBTT_assert(z->ey >= scan_y_top); + // insert at front + z->next = active; + active = z; + } + } + ++e; + } + + // now process all active edges + if (active) + stbtt__fill_active_edges_new(scanline, scanline2+1, result->w, active, scan_y_top); + + { + float sum = 0; + for (i=0; i < result->w; ++i) { + float k; + int m; + sum += scanline2[i]; + k = scanline[i] + sum; + k = (float) STBTT_fabs(k)*255 + 0.5f; + m = (int) k; + if (m > 255) m = 255; + result->pixels[j*result->stride + i] = (unsigned char) m; + } + } + // advance all the edges + step = &active; + while (*step) { + stbtt__active_edge *z = *step; + z->fx += z->fdx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + + ++y; + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#define STBTT__COMPARE(a,b) ((a)->y0 < (b)->y0) + +static void stbtt__sort_edges_ins_sort(stbtt__edge *p, int n) +{ + int i,j; + for (i=1; i < n; ++i) { + stbtt__edge t = p[i], *a = &t; + j = i; + while (j > 0) { + stbtt__edge *b = &p[j-1]; + int c = STBTT__COMPARE(a,b); + if (!c) break; + p[j] = p[j-1]; + --j; + } + if (i != j) + p[j] = t; + } +} + +static void stbtt__sort_edges_quicksort(stbtt__edge *p, int n) +{ + /* threshhold for transitioning to insertion sort */ + while (n > 12) { + stbtt__edge t; + int c01,c12,c,m,i,j; + + /* compute median of three */ + m = n >> 1; + c01 = STBTT__COMPARE(&p[0],&p[m]); + c12 = STBTT__COMPARE(&p[m],&p[n-1]); + /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ + if (c01 != c12) { + /* otherwise, we'll need to swap something else to middle */ + int z; + c = STBTT__COMPARE(&p[0],&p[n-1]); + /* 0>mid && midn => n; 0 0 */ + /* 0n: 0>n => 0; 0 n */ + z = (c == c12) ? 0 : n-1; + t = p[z]; + p[z] = p[m]; + p[m] = t; + } + /* now p[m] is the median-of-three */ + /* swap it to the beginning so it won't move around */ + t = p[0]; + p[0] = p[m]; + p[m] = t; + + /* partition loop */ + i=1; + j=n-1; + for(;;) { + /* handling of equality is crucial here */ + /* for sentinels & efficiency with duplicates */ + for (;;++i) { + if (!STBTT__COMPARE(&p[i], &p[0])) break; + } + for (;;--j) { + if (!STBTT__COMPARE(&p[0], &p[j])) break; + } + /* make sure we haven't crossed */ + if (i >= j) break; + t = p[i]; + p[i] = p[j]; + p[j] = t; + + ++i; + --j; + } + /* recurse on smaller side, iterate on larger */ + if (j < (n-i)) { + stbtt__sort_edges_quicksort(p,j); + p = p+i; + n = n-i; + } else { + stbtt__sort_edges_quicksort(p+i, n-i); + n = j; + } + } +} + +static void stbtt__sort_edges(stbtt__edge *p, int n) +{ + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); +} + +typedef struct +{ + float x,y; +} stbtt__point; + +static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) +{ + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n,i,j,k,m; +#if STBTT_RASTERIZER_VERSION == 1 + int vsubsample = result->h < 8 ? 15 : 5; +#elif STBTT_RASTERIZER_VERSION == 2 + int vsubsample = 1; +#else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i=0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel + if (e == 0) return; + n = 0; + + m=0; + for (i=0; i < windings; ++i) { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i]-1; + for (k=0; k < wcount[i]; j=k++) { + int a=k,b=j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { + e[n].invert = 1; + a=j,b=k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + //STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + stbtt__sort_edges(e, n); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); +} + +static void stbtt__add_point(stbtt__point *points, int n, float x, float y) +{ + if (!points) return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; +} + +// tesselate until threshhold p is happy... @TODO warped to compensate for non-linear stretching +static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) +{ + // midpoint + float mx = (x0 + 2*x1 + x2)/4; + float my = (y0 + 2*y1 + y2)/4; + // versus directly drawn line + float dx = (x0+x2)/2 - mx; + float dy = (y0+y2)/2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x2,y2); + *num_points = *num_points+1; + } + return 1; +} + +// returns number of contours +static stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) +{ + stbtt__point *points=0; + int num_points=0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i,n=0,start=0, pass; + + // count how many "moves" there are to get the contour count + for (i=0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass=0; pass < 2; ++pass) { + float x=0,y=0; + if (pass == 1) { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) goto error; + } + num_points = 0; + n= -1; + for (i=0; i < num_verts; ++i) { + switch (vertices[i].type) { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x,y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; +error: + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; +} + +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) +{ + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count, *winding_lengths; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } +} + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + int ix0,iy0,ix1,iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) { + STBTT_free(vertices, info->userdata); + return NULL; + } + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width ) *width = gbm.w; + if (height) *height = gbm.h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + if (gbm.w && gbm.h) { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) +{ + int ix0,iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; + + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + + STBTT_free(vertices, info->userdata); +} + +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) +{ + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-CRAPPY packing to keep source code small + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x,y,bottom_y, i; + stbtt_fontinfo f; + f.userdata = NULL; + if (!stbtt_InitFont(&f, data, offset)) + return -1; + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + x=y=1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i=0; i < num_chars; ++i) { + int advance, lsb, x0,y0,x1,y1,gw,gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); + gw = x1-x0; + gh = y1-y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x+gw < pw); + STBTT_assert(y+gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 1; + if (y+gh+1 > bottom_y) + bottom_y = y+gh+1; + } + return bottom_y; +} + +STBTT_DEF void stbtt_GetBakedQuad(stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) +{ + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); + + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// rectangle packing replacement routines if you don't have stb_rect_pack.h +// + +#ifndef STB_RECT_PACK_VERSION +#ifdef _MSC_VER +#define STBTT__NOTUSED(v) (void)(v) +#else +#define STBTT__NOTUSED(v) (void)sizeof(v) +#endif + +typedef int stbrp_coord; + +//////////////////////////////////////////////////////////////////////////////////// +// // +// // +// COMPILER WARNING ?!?!? // +// // +// // +// if you get a compile warning due to these symbols being defined more than // +// once, move #include "stb_rect_pack.h" before #include "stb_truetype.h" // +// // +//////////////////////////////////////////////////////////////////////////////////// + +typedef struct +{ + int width,height; + int x,y,bottom_y; +} stbrp_context; + +typedef struct +{ + unsigned char x; +} stbrp_node; + +struct stbrp_rect +{ + stbrp_coord x,y; + int id,w,h,was_packed; +}; + +static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *nodes, int num_nodes) +{ + con->width = pw; + con->height = ph; + con->x = 0; + con->y = 0; + con->bottom_y = 0; + STBTT__NOTUSED(nodes); + STBTT__NOTUSED(num_nodes); +} + +static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) +{ + int i; + for (i=0; i < num_rects; ++i) { + if (con->x + rects[i].w > con->width) { + con->x = 0; + con->y = con->bottom_y; + } + if (con->y + rects[i].h > con->height) + break; + rects[i].x = con->x; + rects[i].y = con->y; + rects[i].was_packed = 1; + con->x += rects[i].w; + if (con->y + rects[i].h > con->bottom_y) + con->bottom_y = con->y + rects[i].h; + } + for ( ; i < num_rects; ++i) + rects[i].was_packed = 0; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-AWESOME (tm Ryan Gordon) packing using stb_rect_pack.h. If +// stb_rect_pack.h isn't available, it uses the BakeFontBitmap strategy. + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int pw, int ph, int stride_in_bytes, int padding, void *alloc_context) +{ + stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context) ,alloc_context); + int num_nodes = pw - padding; + stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes ) * num_nodes,alloc_context); + + if (context == NULL || nodes == NULL) { + if (context != NULL) STBTT_free(context, alloc_context); + if (nodes != NULL) STBTT_free(nodes , alloc_context); + return 0; + } + + spc->user_allocator_context = alloc_context; + spc->width = pw; + spc->height = ph; + spc->pixels = pixels; + spc->pack_info = context; + spc->nodes = nodes; + spc->padding = padding; + spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; + spc->h_oversample = 1; + spc->v_oversample = 1; + + stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + + if (pixels) + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + + return 1; +} + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc) +{ + STBTT_free(spc->nodes , spc->user_allocator_context); + STBTT_free(spc->pack_info, spc->user_allocator_context); +} + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample) +{ + STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); + STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); + if (h_oversample <= STBTT_MAX_OVERSAMPLE) + spc->h_oversample = h_oversample; + if (v_oversample <= STBTT_MAX_OVERSAMPLE) + spc->v_oversample = v_oversample; +} + +#define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE-1) + +static void stbtt__h_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_w = w - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < h; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < w; ++i) { + STBTT_assert(pixels[i] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i] = (unsigned char) (total / kernel_width); + } + + pixels += stride_in_bytes; + } +} + +static void stbtt__v_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_h = h - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < w; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < h; ++i) { + STBTT_assert(pixels[i*stride_in_bytes] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + + pixels += 1; + } +} + +static float stbtt__oversample_shift(int oversample) +{ + if (!oversample) + return 0.0f; + + // The prefilter is a box filter of width "oversample", + // which shifts phase by (oversample - 1)/2 pixels in + // oversampled space. We want to shift in the opposite + // direction to counter this. + return (float)-(oversample - 1) / (2.0f * (float)oversample); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k; + + k=0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + ranges[i].h_oversample = (unsigned char) spc->h_oversample; + ranges[i].v_oversample = (unsigned char) spc->v_oversample; + for (j=0; j < ranges[i].num_chars; ++j) { + int x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + &x0,&y0,&x1,&y1); + rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); + rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); + ++k; + } + } + + return k; +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k, return_value = 1; + + // save current values + int old_h_over = spc->h_oversample; + int old_v_over = spc->v_oversample; + + k = 0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + float recip_h,recip_v,sub_x,sub_y; + spc->h_oversample = ranges[i].h_oversample; + spc->v_oversample = ranges[i].v_oversample; + recip_h = 1.0f / spc->h_oversample; + recip_v = 1.0f / spc->v_oversample; + sub_x = stbtt__oversample_shift(spc->h_oversample); + sub_y = stbtt__oversample_shift(spc->v_oversample); + for (j=0; j < ranges[i].num_chars; ++j) { + stbrp_rect *r = &rects[k]; + if (r->was_packed) { + stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; + int advance, lsb, x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbrp_coord pad = (stbrp_coord) spc->padding; + + // pad on left and top + r->x += pad; + r->y += pad; + r->w -= pad; + r->h -= pad; + stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); + stbtt_GetGlyphBitmapBox(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + &x0,&y0,&x1,&y1); + stbtt_MakeGlyphBitmapSubpixel(info, + spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w - spc->h_oversample+1, + r->h - spc->v_oversample+1, + spc->stride_in_bytes, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + glyph); + + if (spc->h_oversample > 1) + stbtt__h_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->h_oversample); + + if (spc->v_oversample > 1) + stbtt__v_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->v_oversample); + + bc->x0 = (stbtt_int16) r->x; + bc->y0 = (stbtt_int16) r->y; + bc->x1 = (stbtt_int16) (r->x + r->w); + bc->y1 = (stbtt_int16) (r->y + r->h); + bc->xadvance = scale * advance; + bc->xoff = (float) x0 * recip_h + sub_x; + bc->yoff = (float) y0 * recip_v + sub_y; + bc->xoff2 = (x0 + r->w) * recip_h + sub_x; + bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + } else { + return_value = 0; // if any fail, report failure + } + + ++k; + } + } + + // restore original values + spc->h_oversample = old_h_over; + spc->v_oversample = old_v_over; + + return return_value; +} + +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects) +{ + stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); +} + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) +{ + stbtt_fontinfo info; + int i,j,n, return_value = 1; + //stbrp_context *context = (stbrp_context *) spc->pack_info; + stbrp_rect *rects; + + // flag all characters as NOT packed + for (i=0; i < num_ranges; ++i) + for (j=0; j < ranges[i].num_chars; ++j) + ranges[i].chardata_for_range[j].x0 = + ranges[i].chardata_for_range[j].y0 = + ranges[i].chardata_for_range[j].x1 = + ranges[i].chardata_for_range[j].y1 = 0; + + n = 0; + for (i=0; i < num_ranges; ++i) + n += ranges[i].num_chars; + + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); + if (rects == NULL) + return 0; + + info.userdata = spc->user_allocator_context; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata,font_index)); + + n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); + + stbtt_PackFontRangesPackRects(spc, rects, n); + + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + + STBTT_free(rects, spc->user_allocator_context); + return return_value; +} + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, float font_size, + int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) +{ + stbtt_pack_range range; + range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; + range.array_of_unicode_codepoints = NULL; + range.num_chars = num_chars_in_range; + range.chardata_for_range = chardata_for_range; + range.font_size = font_size; + return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); +} + +STBTT_DEF void stbtt_GetPackedQuad(stbtt_packedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int align_to_integer) +{ + float ipw = 1.0f / pw, iph = 1.0f / ph; + stbtt_packedchar *b = chardata + char_index; + + if (align_to_integer) { + float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); + float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); + q->x0 = x; + q->y0 = y; + q->x1 = x + b->xoff2 - b->xoff; + q->y1 = y + b->yoff2 - b->yoff; + } else { + q->x0 = *xpos + b->xoff; + q->y0 = *ypos + b->yoff; + q->x1 = *xpos + b->xoff2; + q->y1 = *ypos + b->yoff2; + } + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + + +////////////////////////////////////////////////////////////////////////////// +// +// font name matching -- recommended not to use this +// + +// check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(const stbtt_uint8 *s1, stbtt_int32 len1, const stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i=0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) { + stbtt_uint16 ch = s2[0]*256 + s2[1]; + if (ch < 0x80) { + if (i >= len1) return -1; + if (s1[i++] != ch) return -1; + } else if (ch < 0x800) { + if (i+1 >= len1) return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; + } else if (ch >= 0xd800 && ch < 0xdc00) { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2]*256 + s2[3]; + if (i+3 >= len1) return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } else if (ch >= 0xdc00 && ch < 0xe000) { + return -1; + } else { + if (i+2 >= len1) return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((const stbtt_uint8*) s1, len1, (const stbtt_uint8*) s2, len2); +} + +// returns results in whatever encoding you request... but note that 2-byte encodings +// will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) +{ + stbtt_int32 i,count,stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return NULL; + + count = ttUSHORT(fc+nm+2); + stringOffset = nm + ttUSHORT(fc+nm+4); + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) + && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { + *length = ttUSHORT(fc+loc+8); + return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); + } + } + return NULL; +} + +static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) +{ + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc+nm+2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); + + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc+loc+6); + if (id == target_id) { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { + stbtt_int32 slen = ttUSHORT(fc+loc+8); + stbtt_int32 off = ttUSHORT(fc+loc+10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); + if (matchlen >= 0) { + // check for target_id+1 immediately following, with same encoding & language + if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { + slen = ttUSHORT(fc+loc+12+8); + off = ttUSHORT(fc+loc+12+10); + if (slen == 0) { + if (matchlen == nlen) + return 1; + } else if (matchlen < nlen && name[matchlen] == ' ') { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) + return 1; + } + } else { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; +} + +static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) +{ + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm,hd; + if (!stbtt__isfont(fc+offset)) return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return 0; + + if (flags) { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } else { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } + + return 0; +} + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *font_collection, const char *name_utf8, stbtt_int32 flags) +{ + stbtt_int32 i; + for (i=0;;++i) { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) + return off; + } +} + +#endif // STB_TRUETYPE_IMPLEMENTATION + + +// FULL VERSION HISTORY +// +// 1.10 (2016-04-02) allow user-defined fabs() replacement +// fix memory leak if fontsize=0.0 +// fix warning from duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use alloc userdata for PackFontRanges +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// allow PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// 1.02 (2014-12-10) fix various warnings & compile issues w/ stb_rect_pack, C++ +// 1.01 (2014-12-08) fix subpixel position when oversampling to exactly match +// non-oversampled; STBTT_POINT_SIZE for packed case only +// 1.00 (2014-12-06) add new PackBegin etc. API, w/ support for oversampling +// 0.99 (2014-09-18) fix multiple bugs with subpixel rendering (ryg) +// 0.9 (2014-08-07) support certain mac/iOS fonts without an MS platformID +// 0.8b (2014-07-07) fix a warning +// 0.8 (2014-05-25) fix a few more warnings +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (stb) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp new file mode 100644 index 00000000..063d50c9 --- /dev/null +++ b/attachments/simple_engine/imgui_system.cpp @@ -0,0 +1,730 @@ +#include "imgui_system.h" +#include "renderer.h" + +// Include ImGui headers +#include "imgui/imgui.h" + +#include + +// This implementation corresponds to the GUI chapter in the tutorial: +// @see en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc + +ImGuiSystem::ImGuiSystem() { + // Constructor implementation +} + +ImGuiSystem::~ImGuiSystem() { + // Destructor implementation + Cleanup(); +} + +bool ImGuiSystem::Initialize(Renderer* renderer, uint32_t width, uint32_t height) { + if (initialized) { + return true; + } + + this->renderer = renderer; + this->width = width; + this->height = height; + + // Create ImGui context + context = ImGui::CreateContext(); + if (!context) { + std::cerr << "Failed to create ImGui context" << std::endl; + return false; + } + + // Configure ImGui + ImGuiIO& io = ImGui::GetIO(); + // Set display size + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Create Vulkan resources + if (!createResources()) { + std::cerr << "Failed to create ImGui Vulkan resources" << std::endl; + Cleanup(); + return false; + } + + initialized = true; + return true; +} + +void ImGuiSystem::Cleanup() { + if (!initialized) { + return; + } + + // Wait for the device to be idle before cleaning up + if (renderer) { + renderer->WaitIdle(); + } + + // Clean up Vulkan resources - using RAII, these will be automatically destroyed + pipeline = nullptr; + pipelineLayout = nullptr; + descriptorSetLayout = nullptr; + fontSampler = nullptr; + fontView = nullptr; + fontImage = nullptr; + fontMemory = nullptr; + vertexBuffer = nullptr; + vertexBufferMemory = nullptr; + indexBuffer = nullptr; + indexBufferMemory = nullptr; + descriptorPool = nullptr; + + // Destroy ImGui context + if (context) { + ImGui::DestroyContext(context); + context = nullptr; + } + + initialized = false; +} + +void ImGuiSystem::NewFrame() { + if (!initialized) { + return; + } + + ImGui::NewFrame(); + + // Create your UI elements here + // For example: + ImGui::Begin("Simple Engine Demo"); + ImGui::Text("Hello, Vulkan!"); + if (ImGui::Button("Click me!")) { + // Handle button click + } + ImGui::End(); +} + +void ImGuiSystem::Render(vk::CommandBuffer commandBuffer) { + if (!initialized) { + return; + } + + // End the frame and prepare for rendering + ImGui::Render(); + + // Update vertex and index buffers + updateBuffers(); + + // Record rendering commands + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + try { + // Bind the pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pipeline); + + // Set viewport + vk::Viewport viewport; + viewport.width = ImGui::GetIO().DisplaySize.x; + viewport.height = ImGui::GetIO().DisplaySize.y; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + commandBuffer.setViewport(0, {viewport}); + + // Set push constants + struct PushConstBlock { + float scale[2]; + float translate[2]; + } pushConstBlock; + + pushConstBlock.scale[0] = 2.0f / ImGui::GetIO().DisplaySize.x; + pushConstBlock.scale[1] = 2.0f / ImGui::GetIO().DisplaySize.y; + pushConstBlock.translate[0] = -1.0f; + pushConstBlock.translate[1] = -1.0f; + + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(PushConstBlock), &pushConstBlock); + + // Bind vertex and index buffers + std::array vertexBuffers = {*vertexBuffer}; + std::array offsets = {0}; + commandBuffer.bindVertexBuffers(0, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint16); + + // Render command lists + int vertexOffset = 0; + int indexOffset = 0; + + for (int i = 0; i < drawData->CmdListsCount; i++) { + const ImDrawList* cmdList = drawData->CmdLists[i]; + + for (int j = 0; j < cmdList->CmdBuffer.Size; j++) { + const ImDrawCmd* pcmd = &cmdList->CmdBuffer[j]; + + // Set scissor rectangle + vk::Rect2D scissor; + scissor.offset.x = std::max(static_cast(pcmd->ClipRect.x), 0); + scissor.offset.y = std::max(static_cast(pcmd->ClipRect.y), 0); + scissor.extent.width = static_cast(pcmd->ClipRect.z - pcmd->ClipRect.x); + scissor.extent.height = static_cast(pcmd->ClipRect.w - pcmd->ClipRect.y); + commandBuffer.setScissor(0, {scissor}); + + // Bind descriptor set (font texture) + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {descriptorSet}, {}); + + // Draw + commandBuffer.drawIndexed(pcmd->ElemCount, 1, indexOffset, vertexOffset, 0); + indexOffset += pcmd->ElemCount; + } + + vertexOffset += cmdList->VtxBuffer.Size; + } + } catch (const std::exception& e) { + std::cerr << "Failed to render ImGui: " << e.what() << std::endl; + } +} + +void ImGuiSystem::HandleMouse(float x, float y, uint32_t buttons) { + if (!initialized) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + + // Update mouse position + io.MousePos = ImVec2(x, y); + + // Update mouse buttons + io.MouseDown[0] = (buttons & 0x01) != 0; // Left button + io.MouseDown[1] = (buttons & 0x02) != 0; // Right button + io.MouseDown[2] = (buttons & 0x04) != 0; // Middle button +} + +void ImGuiSystem::HandleKeyboard(uint32_t key, bool pressed) { + if (!initialized) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + + // Update key state + if (key < 512) { + io.KeysDown[key] = pressed; + } + + // Update modifier keys + // Using GLFW key codes instead of Windows-specific VK_* constants + io.KeyCtrl = io.KeysDown[341] || io.KeysDown[345]; // Left/Right Control + io.KeyShift = io.KeysDown[340] || io.KeysDown[344]; // Left/Right Shift + io.KeyAlt = io.KeysDown[342] || io.KeysDown[346]; // Left/Right Alt + io.KeySuper = io.KeysDown[343] || io.KeysDown[347]; // Left/Right Super +} + +void ImGuiSystem::HandleChar(uint32_t c) { + if (!initialized) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + io.AddInputCharacter(c); +} + +void ImGuiSystem::HandleResize(uint32_t width, uint32_t height) { + if (!initialized) { + return; + } + + this->width = width; + this->height = height; + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); +} + +bool ImGuiSystem::WantCaptureKeyboard() const { + if (!initialized) { + return false; + } + + return ImGui::GetIO().WantCaptureKeyboard; +} + +bool ImGuiSystem::WantCaptureMouse() const { + if (!initialized) { + return false; + } + + return ImGui::GetIO().WantCaptureMouse; +} + +bool ImGuiSystem::createResources() { + // Create all Vulkan resources needed for ImGui rendering + if (!createFontTexture()) { + return false; + } + + if (!createDescriptorSetLayout()) { + return false; + } + + if (!createDescriptorPool()) { + return false; + } + + if (!createDescriptorSet()) { + return false; + } + + if (!createPipelineLayout()) { + return false; + } + + if (!createPipeline()) { + return false; + } + + return true; +} + +bool ImGuiSystem::createFontTexture() { + // Get font texture from ImGui + ImGuiIO& io = ImGui::GetIO(); + unsigned char* fontData; + int texWidth, texHeight; + io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight); + vk::DeviceSize uploadSize = texWidth * texHeight * 4 * sizeof(char); + + try { + // Create the font image + vk::ImageCreateInfo imageInfo; + imageInfo.imageType = vk::ImageType::e2D; + imageInfo.format = vk::Format::eR8G8B8A8Unorm; + imageInfo.extent.width = static_cast(texWidth); + imageInfo.extent.height = static_cast(texHeight); + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = vk::SampleCountFlagBits::e1; + imageInfo.tiling = vk::ImageTiling::eOptimal; + imageInfo.usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst; + imageInfo.sharingMode = vk::SharingMode::eExclusive; + imageInfo.initialLayout = vk::ImageLayout::eUndefined; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + fontImage = vk::raii::Image(device, imageInfo); + + // Allocate memory for the image + vk::MemoryRequirements memRequirements = fontImage.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal); + + fontMemory = vk::raii::DeviceMemory(device, allocInfo); + fontImage.bindMemory(*fontMemory, 0); + + // Create a staging buffer for uploading the font data + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = uploadSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eTransferSrc; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + vk::raii::Buffer stagingBuffer(device, bufferInfo); + + vk::MemoryRequirements stagingMemRequirements = stagingBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo stagingAllocInfo; + stagingAllocInfo.allocationSize = stagingMemRequirements.size; + stagingAllocInfo.memoryTypeIndex = renderer->FindMemoryType(stagingMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vk::raii::DeviceMemory stagingBufferMemory(device, stagingAllocInfo); + stagingBuffer.bindMemory(*stagingBufferMemory, 0); + + // Copy font data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, uploadSize); + memcpy(data, fontData, uploadSize); + stagingBufferMemory.unmapMemory(); + + // Transition image layout and copy data + renderer->TransitionImageLayout(*fontImage, vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + renderer->CopyBufferToImage(*stagingBuffer, *fontImage, + static_cast(texWidth), static_cast(texHeight)); + renderer->TransitionImageLayout(*fontImage, vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Staging buffer and memory will be automatically cleaned up by RAII + + // Create image view + vk::ImageViewCreateInfo viewInfo; + viewInfo.image = *fontImage; + viewInfo.viewType = vk::ImageViewType::e2D; + viewInfo.format = vk::Format::eR8G8B8A8Unorm; + viewInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + fontView = vk::raii::ImageView(device, viewInfo); + + // Create sampler + vk::SamplerCreateInfo samplerInfo; + samplerInfo.magFilter = vk::Filter::eLinear; + samplerInfo.minFilter = vk::Filter::eLinear; + samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; + samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.mipLodBias = 0.0f; + samplerInfo.anisotropyEnable = VK_FALSE; + samplerInfo.maxAnisotropy = 1.0f; + samplerInfo.compareEnable = VK_FALSE; + samplerInfo.compareOp = vk::CompareOp::eAlways; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 0.0f; + samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; + samplerInfo.unnormalizedCoordinates = VK_FALSE; + + fontSampler = vk::raii::Sampler(device, samplerInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create font texture: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorSetLayout() { + try { + vk::DescriptorSetLayoutBinding binding; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; + binding.descriptorCount = 1; + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + binding.binding = 0; + + vk::DescriptorSetLayoutCreateInfo layoutInfo; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorPool() { + try { + vk::DescriptorPoolSize poolSize; + poolSize.type = vk::DescriptorType::eCombinedImageSampler; + poolSize.descriptorCount = 1; + + vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorSet() { + try { + vk::DescriptorSetAllocateInfo allocInfo; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &(*descriptorSetLayout); + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + auto descriptorSets = device.allocateDescriptorSets(allocInfo); + descriptorSet = descriptorSets[0]; // Store the first (and only) descriptor set + + // Update descriptor set + vk::DescriptorImageInfo imageInfo; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + imageInfo.imageView = *fontView; + imageInfo.sampler = *fontSampler; + + vk::WriteDescriptorSet writeSet; + writeSet.dstSet = descriptorSet; + writeSet.descriptorCount = 1; + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; + writeSet.pImageInfo = &imageInfo; + writeSet.dstBinding = 0; + + device.updateDescriptorSets({writeSet}, {}); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createPipelineLayout() { + try { + // Push constant range for the transformation matrix + vk::PushConstantRange pushConstantRange; + pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(float) * 4; // 2 floats for scale, 2 floats for translate + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &(*descriptorSetLayout); + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create pipeline layout: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createPipeline() { + try { + // Load shaders + vk::raii::ShaderModule vertShaderModule = renderer->CreateShaderModule("shaders/imgui.spv"); + vk::raii::ShaderModule fragShaderModule = renderer->CreateShaderModule("shaders/imgui.spv"); + + // Shader stage creation + vk::PipelineShaderStageCreateInfo vertShaderStageInfo; + vertShaderStageInfo.stage = vk::ShaderStageFlagBits::eVertex; + vertShaderStageInfo.module = *vertShaderModule; + vertShaderStageInfo.pName = "VSMain"; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo; + fragShaderStageInfo.stage = vk::ShaderStageFlagBits::eFragment; + fragShaderStageInfo.module = *fragShaderModule; + fragShaderStageInfo.pName = "PSMain"; + + std::array shaderStages = {vertShaderStageInfo, fragShaderStageInfo}; + + // Vertex input + vk::VertexInputBindingDescription bindingDescription; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(ImDrawVert); + bindingDescription.inputRate = vk::VertexInputRate::eVertex; + + std::array attributeDescriptions; + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = vk::Format::eR32G32Sfloat; + attributeDescriptions[0].offset = offsetof(ImDrawVert, pos); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = vk::Format::eR32G32Sfloat; + attributeDescriptions[1].offset = offsetof(ImDrawVert, uv); + + attributeDescriptions[2].binding = 0; + attributeDescriptions[2].location = 2; + attributeDescriptions[2].format = vk::Format::eR8G8B8A8Unorm; + attributeDescriptions[2].offset = offsetof(ImDrawVert, col); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo; + vertexInputInfo.vertexBindingDescriptionCount = 1; + vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; + vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); + vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); + + // Input assembly + vk::PipelineInputAssemblyStateCreateInfo inputAssembly; + inputAssembly.topology = vk::PrimitiveTopology::eTriangleList; + inputAssembly.primitiveRestartEnable = VK_FALSE; + + // Viewport and scissor + vk::PipelineViewportStateCreateInfo viewportState; + viewportState.viewportCount = 1; + viewportState.scissorCount = 1; + viewportState.pViewports = nullptr; // Dynamic state + viewportState.pScissors = nullptr; // Dynamic state + + // Rasterization + vk::PipelineRasterizationStateCreateInfo rasterizer; + rasterizer.depthClampEnable = VK_FALSE; + rasterizer.rasterizerDiscardEnable = VK_FALSE; + rasterizer.polygonMode = vk::PolygonMode::eFill; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = vk::CullModeFlagBits::eNone; + rasterizer.frontFace = vk::FrontFace::eCounterClockwise; + rasterizer.depthBiasEnable = VK_FALSE; + + // Multisampling + vk::PipelineMultisampleStateCreateInfo multisampling; + multisampling.sampleShadingEnable = VK_FALSE; + multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1; + + // Depth and stencil testing + vk::PipelineDepthStencilStateCreateInfo depthStencil; + depthStencil.depthTestEnable = VK_FALSE; + depthStencil.depthWriteEnable = VK_FALSE; + depthStencil.depthCompareOp = vk::CompareOp::eLessOrEqual; + depthStencil.depthBoundsTestEnable = VK_FALSE; + depthStencil.stencilTestEnable = VK_FALSE; + + // Color blending + vk::PipelineColorBlendAttachmentState colorBlendAttachment; + colorBlendAttachment.colorWriteMask = + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + colorBlendAttachment.blendEnable = VK_TRUE; + colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; + colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd; + colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eZero; + colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd; + + vk::PipelineColorBlendStateCreateInfo colorBlending; + colorBlending.logicOpEnable = VK_FALSE; + colorBlending.attachmentCount = 1; + colorBlending.pAttachments = &colorBlendAttachment; + + // Dynamic state + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState; + dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); + dynamicState.pDynamicStates = dynamicStates.data(); + + // Create the graphics pipeline with dynamic rendering + vk::PipelineRenderingCreateInfo renderingInfo; + renderingInfo.colorAttachmentCount = 1; + vk::Format colorFormat = vk::Format::eR8G8B8A8Unorm; // Use the format of your swapchain images + renderingInfo.pColorAttachmentFormats = &colorFormat; + + vk::GraphicsPipelineCreateInfo pipelineInfo; + pipelineInfo.stageCount = static_cast(shaderStages.size()); + pipelineInfo.pStages = shaderStages.data(); + pipelineInfo.pVertexInputState = &vertexInputInfo; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterizer; + pipelineInfo.pMultisampleState = &multisampling; + pipelineInfo.pDepthStencilState = &depthStencil; + pipelineInfo.pColorBlendState = &colorBlending; + pipelineInfo.pDynamicState = &dynamicState; + pipelineInfo.layout = *pipelineLayout; + pipelineInfo.pNext = &renderingInfo; + pipelineInfo.basePipelineHandle = nullptr; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + auto pipelines = device.createGraphicsPipelines(nullptr, {pipelineInfo}); + pipeline = std::move(pipelines[0]); + + // Shader modules will be automatically cleaned up by RAII + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; + return false; + } +} + +void ImGuiSystem::updateBuffers() { + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + try { + const vk::raii::Device& device = renderer->GetRaiiDevice(); + + // Calculate required buffer sizes + vk::DeviceSize vertexBufferSize = drawData->TotalVtxCount * sizeof(ImDrawVert); + vk::DeviceSize indexBufferSize = drawData->TotalIdxCount * sizeof(ImDrawIdx); + + // Resize buffers if needed + if (drawData->TotalVtxCount > vertexCount) { + // Clean up old buffer - RAII will handle this automatically + vertexBuffer = nullptr; + vertexBufferMemory = nullptr; + + // Create new vertex buffer + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = vertexBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + vertexBuffer = vk::raii::Buffer(device, bufferInfo); + + vk::MemoryRequirements memRequirements = vertexBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vertexBufferMemory = vk::raii::DeviceMemory(device, allocInfo); + vertexBuffer.bindMemory(*vertexBufferMemory, 0); + vertexCount = drawData->TotalVtxCount; + } + + if (drawData->TotalIdxCount > indexCount) { + // Clean up old buffer - RAII will handle this automatically + indexBuffer = nullptr; + indexBufferMemory = nullptr; + + // Create new index buffer + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = indexBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eIndexBuffer; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + indexBuffer = vk::raii::Buffer(device, bufferInfo); + + vk::MemoryRequirements memRequirements = indexBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + indexBufferMemory = vk::raii::DeviceMemory(device, allocInfo); + indexBuffer.bindMemory(*indexBufferMemory, 0); + indexCount = drawData->TotalIdxCount; + } + + // Upload data to buffers + void* vtxMappedMemory = vertexBufferMemory.mapMemory(0, vertexBufferSize); + void* idxMappedMemory = indexBufferMemory.mapMemory(0, indexBufferSize); + + ImDrawVert* vtxDst = static_cast(vtxMappedMemory); + ImDrawIdx* idxDst = static_cast(idxMappedMemory); + + for (int n = 0; n < drawData->CmdListsCount; n++) { + const ImDrawList* cmdList = drawData->CmdLists[n]; + memcpy(vtxDst, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy(idxDst, cmdList->IdxBuffer.Data, cmdList->IdxBuffer.Size * sizeof(ImDrawIdx)); + vtxDst += cmdList->VtxBuffer.Size; + idxDst += cmdList->IdxBuffer.Size; + } + + vertexBufferMemory.unmapMemory(); + indexBufferMemory.unmapMemory(); + } catch (const std::exception& e) { + std::cerr << "Failed to update buffers: " << e.what() << std::endl; + } +} diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h new file mode 100644 index 00000000..1c99ab35 --- /dev/null +++ b/attachments/simple_engine/imgui_system.h @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include + +// Forward declarations +class Renderer; +struct ImGuiContext; + +/** + * @brief Class for managing ImGui integration with Vulkan. + * + * This class implements the ImGui integration as described in the GUI chapter: + * @see en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc + */ +class ImGuiSystem { +public: + /** + * @brief Default constructor. + */ + ImGuiSystem(); + + /** + * @brief Destructor for proper cleanup. + */ + ~ImGuiSystem(); + + /** + * @brief Initialize the ImGui system. + * @param renderer Pointer to the renderer. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(Renderer* renderer, uint32_t width, uint32_t height); + + /** + * @brief Clean up ImGui resources. + */ + void Cleanup(); + + /** + * @brief Start a new ImGui frame. + */ + void NewFrame(); + + /** + * @brief Render the ImGui frame. + * @param commandBuffer The command buffer to record rendering commands to. + */ + void Render(vk::CommandBuffer commandBuffer); + + /** + * @brief Handle mouse input. + * @param x The x-coordinate of the mouse. + * @param y The y-coordinate of the mouse. + * @param buttons The state of the mouse buttons. + */ + void HandleMouse(float x, float y, uint32_t buttons); + + /** + * @brief Handle keyboard input. + * @param key The key code. + * @param pressed Whether the key was pressed or released. + */ + void HandleKeyboard(uint32_t key, bool pressed); + + /** + * @brief Handle character input. + * @param c The character. + */ + void HandleChar(uint32_t c); + + /** + * @brief Handle window resize. + * @param width The new width of the window. + * @param height The new height of the window. + */ + void HandleResize(uint32_t width, uint32_t height); + + /** + * @brief Check if ImGui wants to capture keyboard input. + * @return True if ImGui wants to capture keyboard input, false otherwise. + */ + bool WantCaptureKeyboard() const; + + /** + * @brief Check if ImGui wants to capture mouse input. + * @return True if ImGui wants to capture mouse input, false otherwise. + */ + bool WantCaptureMouse() const; + +private: + // ImGui context + ImGuiContext* context = nullptr; + + // Renderer reference + Renderer* renderer = nullptr; + + // Vulkan resources + vk::raii::DescriptorPool descriptorPool = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::DescriptorSet descriptorSet = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline pipeline = nullptr; + vk::raii::Sampler fontSampler = nullptr; + vk::raii::Image fontImage = nullptr; + vk::raii::DeviceMemory fontMemory = nullptr; + vk::raii::ImageView fontView = nullptr; + vk::raii::Buffer vertexBuffer = nullptr; + vk::raii::DeviceMemory vertexBufferMemory = nullptr; + vk::raii::Buffer indexBuffer = nullptr; + vk::raii::DeviceMemory indexBufferMemory = nullptr; + uint32_t vertexCount = 0; + uint32_t indexCount = 0; + + // Window dimensions + uint32_t width = 0; + uint32_t height = 0; + + // Mouse state + float mouseX = 0.0f; + float mouseY = 0.0f; + uint32_t mouseButtons = 0; + + // Initialization flag + bool initialized = false; + + /** + * @brief Create Vulkan resources for ImGui. + * @return True if creation was successful, false otherwise. + */ + bool createResources(); + + /** + * @brief Create font texture. + * @return True if creation was successful, false otherwise. + */ + bool createFontTexture(); + + /** + * @brief Create descriptor set layout. + * @return True if creation was successful, false otherwise. + */ + bool createDescriptorSetLayout(); + + /** + * @brief Create descriptor pool. + * @return True if creation was successful, false otherwise. + */ + bool createDescriptorPool(); + + /** + * @brief Create descriptor set. + * @return True if creation was successful, false otherwise. + */ + bool createDescriptorSet(); + + /** + * @brief Create pipeline layout. + * @return True if creation was successful, false otherwise. + */ + bool createPipelineLayout(); + + /** + * @brief Create pipeline. + * @return True if creation was successful, false otherwise. + */ + bool createPipeline(); + + /** + * @brief Update vertex and index buffers. + */ + void updateBuffers(); +}; diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp new file mode 100644 index 00000000..b6ec0bae --- /dev/null +++ b/attachments/simple_engine/main.cpp @@ -0,0 +1,138 @@ +#include "engine.h" +#include "transform_component.h" +#include "mesh_component.h" +#include "camera_component.h" + +#include +#include + +// Constants +constexpr int WINDOW_WIDTH = 800; +constexpr int WINDOW_HEIGHT = 600; +constexpr bool ENABLE_VALIDATION_LAYERS = true; + +/** + * @brief Set up a simple scene with a camera and some objects. + * @param engine The engine to set up the scene in. + */ +void SetupScene(Engine* engine) { + // Create a camera entity + Entity* cameraEntity = engine->CreateEntity("Camera"); + if (!cameraEntity) { + throw std::runtime_error("Failed to create camera entity"); + } + + // Add transform component to the camera + TransformComponent* cameraTransform = cameraEntity->AddComponent(); + cameraTransform->SetPosition(glm::vec3(0.0f, 0.0f, 3.0f)); + + // Add camera component to the camera entity + CameraComponent* camera = cameraEntity->AddComponent(); + camera->SetAspectRatio(static_cast(WINDOW_WIDTH) / static_cast(WINDOW_HEIGHT)); + + // Set the camera as the active camera + engine->SetActiveCamera(camera); + + // Create a cube entity + Entity* cubeEntity = engine->CreateEntity("Cube"); + if (!cubeEntity) { + throw std::runtime_error("Failed to create cube entity"); + } + + // Add transform component to the cube + TransformComponent* cubeTransform = cubeEntity->AddComponent(); + cubeTransform->SetPosition(glm::vec3(0.0f, 0.0f, 0.0f)); + cubeTransform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); + cubeTransform->SetScale(glm::vec3(1.0f, 1.0f, 1.0f)); + + // Add mesh component to the cube + MeshComponent* cubeMesh = cubeEntity->AddComponent(); + cubeMesh->CreateCube(1.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + // Create a second cube entity + Entity* cube2Entity = engine->CreateEntity("Cube2"); + if (!cube2Entity) { + throw std::runtime_error("Failed to create second cube entity"); + } + + // Add transform component to the second cube + TransformComponent* cube2Transform = cube2Entity->AddComponent(); + cube2Transform->SetPosition(glm::vec3(2.0f, 0.0f, 0.0f)); + cube2Transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); + cube2Transform->SetScale(glm::vec3(0.5f, 0.5f, 0.5f)); + + // Add mesh component to the second cube + MeshComponent* cube2Mesh = cube2Entity->AddComponent(); + cube2Mesh->CreateCube(1.0f, glm::vec3(0.0f, 1.0f, 0.0f)); + + // Create a third cube entity + Entity* cube3Entity = engine->CreateEntity("Cube3"); + if (!cube3Entity) { + throw std::runtime_error("Failed to create third cube entity"); + } + + // Add transform component to the third cube + TransformComponent* cube3Transform = cube3Entity->AddComponent(); + cube3Transform->SetPosition(glm::vec3(-2.0f, 0.0f, 0.0f)); + cube3Transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); + cube3Transform->SetScale(glm::vec3(0.5f, 0.5f, 0.5f)); + + // Add mesh component to the third cube + MeshComponent* cube3Mesh = cube3Entity->AddComponent(); + cube3Mesh->CreateCube(1.0f, glm::vec3(0.0f, 0.0f, 1.0f)); +} + +#if PLATFORM_ANDROID +/** + * @brief Android entry point. + * @param app The Android app. + */ +void android_main(android_app* app) { + try { + // Create the engine + Engine engine; + + // Initialize the engine + if (!engine.InitializeAndroid(app, "Simple Engine", ENABLE_VALIDATION_LAYERS)) { + throw std::runtime_error("Failed to initialize engine"); + } + + // Set up the scene + SetupScene(&engine); + + // Run the engine + engine.RunAndroid(); + } catch (const std::exception& e) { + LOGE("Exception: %s", e.what()); + } +} +#else +/** + * @brief Desktop entry point. + * @param argc The number of command-line arguments. + * @param argv The command-line arguments. + * @return The exit code. + */ +int main(int argc, char* argv[]) { + try { + // Create the engine + Engine engine; + + // Initialize the engine + if (!engine.Initialize("Simple Engine", WINDOW_WIDTH, WINDOW_HEIGHT, ENABLE_VALIDATION_LAYERS)) { + throw std::runtime_error("Failed to initialize engine"); + } + + // Set up the scene + SetupScene(&engine); + + // Run the engine + engine.Run(); + + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception: " << e.what() << std::endl; + return 1; + } +} +#endif diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp new file mode 100644 index 00000000..a35c70e7 --- /dev/null +++ b/attachments/simple_engine/mesh_component.cpp @@ -0,0 +1,78 @@ +#include "mesh_component.h" + +// Most of the MeshComponent class implementation is in the header file +// This file is mainly for any methods that might need additional implementation + +void MeshComponent::CreateQuad(float width, float height, const glm::vec3& color) { + float halfWidth = width * 0.5f; + float halfHeight = height * 0.5f; + + vertices = { + { {-halfWidth, -halfHeight, 0.0f}, color, {0.0f, 0.0f} }, + { { halfWidth, -halfHeight, 0.0f}, color, {1.0f, 0.0f} }, + { { halfWidth, halfHeight, 0.0f}, color, {1.0f, 1.0f} }, + { {-halfWidth, halfHeight, 0.0f}, color, {0.0f, 1.0f} } + }; + + indices = { + 0, 1, 2, + 2, 3, 0 + }; +} + +void MeshComponent::CreateCube(float size, const glm::vec3& color) { + float halfSize = size * 0.5f; + + vertices = { + // Front face + { {-halfSize, -halfSize, halfSize}, color, {0.0f, 0.0f} }, + { { halfSize, -halfSize, halfSize}, color, {1.0f, 0.0f} }, + { { halfSize, halfSize, halfSize}, color, {1.0f, 1.0f} }, + { {-halfSize, halfSize, halfSize}, color, {0.0f, 1.0f} }, + + // Back face + { {-halfSize, -halfSize, -halfSize}, color, {1.0f, 0.0f} }, + { {-halfSize, halfSize, -halfSize}, color, {1.0f, 1.0f} }, + { { halfSize, halfSize, -halfSize}, color, {0.0f, 1.0f} }, + { { halfSize, -halfSize, -halfSize}, color, {0.0f, 0.0f} }, + + // Top face + { {-halfSize, halfSize, -halfSize}, color, {0.0f, 0.0f} }, + { {-halfSize, halfSize, halfSize}, color, {0.0f, 1.0f} }, + { { halfSize, halfSize, halfSize}, color, {1.0f, 1.0f} }, + { { halfSize, halfSize, -halfSize}, color, {1.0f, 0.0f} }, + + // Bottom face + { {-halfSize, -halfSize, -halfSize}, color, {0.0f, 1.0f} }, + { { halfSize, -halfSize, -halfSize}, color, {1.0f, 1.0f} }, + { { halfSize, -halfSize, halfSize}, color, {1.0f, 0.0f} }, + { {-halfSize, -halfSize, halfSize}, color, {0.0f, 0.0f} }, + + // Right face + { { halfSize, -halfSize, -halfSize}, color, {0.0f, 0.0f} }, + { { halfSize, halfSize, -halfSize}, color, {1.0f, 0.0f} }, + { { halfSize, halfSize, halfSize}, color, {1.0f, 1.0f} }, + { { halfSize, -halfSize, halfSize}, color, {0.0f, 1.0f} }, + + // Left face + { {-halfSize, -halfSize, -halfSize}, color, {1.0f, 0.0f} }, + { {-halfSize, -halfSize, halfSize}, color, {0.0f, 0.0f} }, + { {-halfSize, halfSize, halfSize}, color, {0.0f, 1.0f} }, + { {-halfSize, halfSize, -halfSize}, color, {1.0f, 1.0f} } + }; + + indices = { + // Front face + 0, 1, 2, 2, 3, 0, + // Back face + 4, 5, 6, 6, 7, 4, + // Top face + 8, 9, 10, 10, 11, 8, + // Bottom face + 12, 13, 14, 14, 15, 12, + // Right face + 16, 17, 18, 18, 19, 16, + // Left face + 20, 21, 22, 22, 23, 20 + }; +} diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h new file mode 100644 index 00000000..a5088cd5 --- /dev/null +++ b/attachments/simple_engine/mesh_component.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif + +#include "component.h" + +/** + * @brief Structure representing a vertex in a mesh. + */ +struct Vertex { + glm::vec3 position; + glm::vec3 color; + glm::vec2 texCoord; + + bool operator==(const Vertex& other) const { + return position == other.position && + color == other.color && + texCoord == other.texCoord; + } + + static vk::VertexInputBindingDescription getBindingDescription() { + vk::VertexInputBindingDescription bindingDescription{ + .binding = 0, + .stride = sizeof(Vertex), + .inputRate = vk::VertexInputRate::eVertex + }; + return bindingDescription; + } + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions = { + vk::VertexInputAttributeDescription{ + .location = 0, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = offsetof(Vertex, position) + }, + vk::VertexInputAttributeDescription{ + .location = 1, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = offsetof(Vertex, color) + }, + vk::VertexInputAttributeDescription{ + .location = 2, + .binding = 0, + .format = vk::Format::eR32G32Sfloat, + .offset = offsetof(Vertex, texCoord) + } + }; + return attributeDescriptions; + } +}; + +/** + * @brief Component that handles the mesh data for rendering. + */ +class MeshComponent : public Component { +private: + std::vector vertices; + std::vector indices; + std::string texturePath; + + // Vulkan resources will be managed by the renderer + // This component only stores the data + +public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit MeshComponent(const std::string& componentName = "MeshComponent") + : Component(componentName) {} + + /** + * @brief Set the vertices of the mesh. + * @param newVertices The new vertices. + */ + void SetVertices(const std::vector& newVertices) { + vertices = newVertices; + } + + /** + * @brief Get the vertices of the mesh. + * @return The vertices. + */ + const std::vector& GetVertices() const { + return vertices; + } + + /** + * @brief Set the indices of the mesh. + * @param newIndices The new indices. + */ + void SetIndices(const std::vector& newIndices) { + indices = newIndices; + } + + /** + * @brief Get the indices of the mesh. + * @return The indices. + */ + const std::vector& GetIndices() const { + return indices; + } + + /** + * @brief Set the texture path for the mesh. + * @param path The path to the texture file. + */ + void SetTexturePath(const std::string& path) { + texturePath = path; + } + + /** + * @brief Get the texture path for the mesh. + * @return The path to the texture file. + */ + const std::string& GetTexturePath() const { + return texturePath; + } + + /** + * @brief Create a simple quad mesh. + * @param width The width of the quad. + * @param height The height of the quad. + * @param color The color of the quad. + */ + void CreateQuad(float width = 1.0f, float height = 1.0f, const glm::vec3& color = glm::vec3(1.0f)); + + /** + * @brief Create a simple cube mesh. + * @param size The size of the cube. + * @param color The color of the cube. + */ + void CreateCube(float size = 1.0f, const glm::vec3& color = glm::vec3(1.0f)); +}; diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp new file mode 100644 index 00000000..a3a0e2b8 --- /dev/null +++ b/attachments/simple_engine/model_loader.cpp @@ -0,0 +1,317 @@ +#include "model_loader.h" +#include "renderer.h" +#include +#include + +// Forward declarations for classes that will be defined in separate files +class Model { +public: + Model(const std::string& name) : name(name) {} + ~Model() = default; + + const std::string& GetName() const { return name; } + +private: + std::string name; + // Other model data (meshes, materials, etc.) +}; + +class Material { +public: + Material(const std::string& name) : name(name) {} + ~Material() = default; + + const std::string& GetName() const { return name; } + + // PBR properties + glm::vec3 albedo = glm::vec3(1.0f); + float metallic = 0.0f; + float roughness = 1.0f; + float ao = 1.0f; + glm::vec3 emissive = glm::vec3(0.0f); + +private: + std::string name; + // Texture references +}; + +ModelLoader::ModelLoader() { + // Constructor implementation +} + +ModelLoader::~ModelLoader() { + // Destructor implementation + models.clear(); + materials.clear(); +} + +bool ModelLoader::Initialize(Renderer* renderer) { + this->renderer = renderer; + + if (!renderer) { + std::cerr << "ModelLoader::Initialize: Renderer is null" << std::endl; + return false; + } + + std::cout << "ModelLoader initialized successfully" << std::endl; + return true; +} + +Model* ModelLoader::LoadGLTF(const std::string& filename) { + // Check if the model is already loaded + auto it = models.find(filename); + if (it != models.end()) { + return it->second.get(); + } + + // Create a new model + auto model = std::make_unique(filename); + + // Parse the GLTF file + if (!ParseGLTF(filename, model.get())) { + std::cerr << "ModelLoader::LoadGLTF: Failed to parse GLTF file: " << filename << std::endl; + return nullptr; + } + + // Store the model + Model* modelPtr = model.get(); + models[filename] = std::move(model); + + std::cout << "Model loaded successfully: " << filename << std::endl; + return modelPtr; +} + +Model* ModelLoader::LoadGLTFWithPBR(const std::string& filename, + const std::string& albedoMap, + const std::string& normalMap, + const std::string& metallicRoughnessMap, + const std::string& aoMap, + const std::string& emissiveMap) { + // Check if the model is already loaded + auto it = models.find(filename); + if (it != models.end()) { + return it->second.get(); + } + + // Create a new model + auto model = std::make_unique(filename); + + // Parse the GLTF file + if (!ParseGLTF(filename, model.get())) { + std::cerr << "ModelLoader::LoadGLTFWithPBR: Failed to parse GLTF file: " << filename << std::endl; + return nullptr; + } + + // Create a PBR material + auto material = std::make_unique(filename + "_material"); + + // Load PBR textures + if (!LoadPBRTextures(material.get(), albedoMap, normalMap, metallicRoughnessMap, aoMap, emissiveMap)) { + std::cerr << "ModelLoader::LoadGLTFWithPBR: Failed to load PBR textures for model: " << filename << std::endl; + } + + // Store the material + materials[material->GetName()] = std::move(material); + + // Store the model + Model* modelPtr = model.get(); + models[filename] = std::move(model); + + std::cout << "Model with PBR materials loaded successfully: " << filename << std::endl; + return modelPtr; +} + +Model* ModelLoader::GetModel(const std::string& name) { + auto it = models.find(name); + if (it != models.end()) { + return it->second.get(); + } + return nullptr; +} + +Material* ModelLoader::CreatePBRMaterial(const std::string& name, + const glm::vec3& albedo, + float metallic, + float roughness, + float ao, + const glm::vec3& emissive) { + // Check if the material already exists + auto it = materials.find(name); + if (it != materials.end()) { + return it->second.get(); + } + + // Create a new material + auto material = std::make_unique(name); + + // Set PBR properties + material->albedo = albedo; + material->metallic = metallic; + material->roughness = roughness; + material->ao = ao; + material->emissive = emissive; + + // Store the material + Material* materialPtr = material.get(); + materials[name] = std::move(material); + + std::cout << "PBR material created successfully: " << name << std::endl; + return materialPtr; +} + +bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { + // Parse the GLTF file and populate the model + std::cout << "Parsing GLTF file: " << filename << std::endl; + + // Open the file + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + std::cerr << "Failed to open GLTF file: " << filename << std::endl; + return false; + } + + // Read the file content + file.seekg(0, std::ios::end); + size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(fileSize); + file.read(buffer.data(), fileSize); + file.close(); + + // Parse the JSON content + // In a real implementation, this would use a JSON library like nlohmann/json + // For simplicity, we'll just check if the file contains the required GLTF header + std::string content(buffer.begin(), buffer.end()); + if (content.find("\"asset\"") == std::string::npos || + content.find("\"version\"") == std::string::npos) { + std::cerr << "Invalid GLTF file format: " << filename << std::endl; + return false; + } + + // Extract mesh data + // In a real implementation, this would parse the full GLTF structure + // For now, we'll just log what we would extract + std::cout << "Extracting mesh data from GLTF file" << std::endl; + std::cout << " - Vertices" << std::endl; + std::cout << " - Indices" << std::endl; + std::cout << " - Normals" << std::endl; + std::cout << " - Texture coordinates" << std::endl; + std::cout << " - Materials" << std::endl; + + // Extract animation data if present + if (content.find("\"animations\"") != std::string::npos) { + std::cout << "Extracting animation data from GLTF file" << std::endl; + std::cout << " - Keyframes" << std::endl; + std::cout << " - Bone weights" << std::endl; + std::cout << " - Animation channels" << std::endl; + } + + // Extract scene hierarchy + std::cout << "Extracting scene hierarchy from GLTF file" << std::endl; + std::cout << " - Nodes" << std::endl; + std::cout << " - Node hierarchy" << std::endl; + std::cout << " - Node transforms" << std::endl; + + // In a real implementation, we would create meshes, materials, and set up the model + // For now, we'll just return success + return true; +} + +bool ModelLoader::LoadPBRTextures(Material* material, + const std::string& albedoMap, + const std::string& normalMap, + const std::string& metallicRoughnessMap, + const std::string& aoMap, + const std::string& emissiveMap) { + if (!material) { + std::cerr << "ModelLoader::LoadPBRTextures: Material is null" << std::endl; + return false; + } + + if (!renderer) { + std::cerr << "ModelLoader::LoadPBRTextures: Renderer is null" << std::endl; + return false; + } + + std::cout << "Loading PBR textures for material: " << material->GetName() << std::endl; + + bool success = true; + + // Load albedo map + if (!albedoMap.empty()) { + std::cout << " Loading albedo map: " << albedoMap << std::endl; + // In a real implementation, this would load the texture using the renderer + // For example: + // VkImage albedoImage = renderer->LoadTexture(albedoMap); + // if (albedoImage != VK_NULL_HANDLE) { + // material->albedoTexture = albedoImage; + // } else { + // std::cerr << "Failed to load albedo map: " << albedoMap << std::endl; + // success = false; + // } + } + + // Load normal map + if (!normalMap.empty()) { + std::cout << " Loading normal map: " << normalMap << std::endl; + // In a real implementation, this would load the texture using the renderer + // For example: + // VkImage normalImage = renderer->LoadTexture(normalMap); + // if (normalImage != VK_NULL_HANDLE) { + // material->normalTexture = normalImage; + // } else { + // std::cerr << "Failed to load normal map: " << normalMap << std::endl; + // success = false; + // } + } + + // Load metallic-roughness map + if (!metallicRoughnessMap.empty()) { + std::cout << " Loading metallic-roughness map: " << metallicRoughnessMap << std::endl; + // In a real implementation, this would load the texture using the renderer + // For example: + // VkImage metallicRoughnessImage = renderer->LoadTexture(metallicRoughnessMap); + // if (metallicRoughnessImage != VK_NULL_HANDLE) { + // material->metallicRoughnessTexture = metallicRoughnessImage; + // } else { + // std::cerr << "Failed to load metallic-roughness map: " << metallicRoughnessMap << std::endl; + // success = false; + // } + } + + // Load ambient occlusion map + if (!aoMap.empty()) { + std::cout << " Loading ambient occlusion map: " << aoMap << std::endl; + // In a real implementation, this would load the texture using the renderer + // For example: + // VkImage aoImage = renderer->LoadTexture(aoMap); + // if (aoImage != VK_NULL_HANDLE) { + // material->aoTexture = aoImage; + // } else { + // std::cerr << "Failed to load ambient occlusion map: " << aoMap << std::endl; + // success = false; + // } + } + + // Load emissive map + if (!emissiveMap.empty()) { + std::cout << " Loading emissive map: " << emissiveMap << std::endl; + // In a real implementation, this would load the texture using the renderer + // For example: + // VkImage emissiveImage = renderer->LoadTexture(emissiveMap); + // if (emissiveImage != VK_NULL_HANDLE) { + // material->emissiveTexture = emissiveImage; + // } else { + // std::cerr << "Failed to load emissive map: " << emissiveMap << std::endl; + // success = false; + // } + } + + // Set up PBR material properties + // In a real implementation, this would set up the material properties based on the loaded textures + // For example: + // material->SetupPBRPipeline(); + + return success; +} diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h new file mode 100644 index 00000000..498805df --- /dev/null +++ b/attachments/simple_engine/model_loader.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Renderer; +class Mesh; +class Material; +class Model; + +/** + * @brief Class for loading and managing 3D models. + */ +class ModelLoader { +public: + /** + * @brief Default constructor. + */ + ModelLoader(); + + /** + * @brief Destructor for proper cleanup. + */ + ~ModelLoader(); + + /** + * @brief Initialize the model loader. + * @param renderer Pointer to the renderer. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(Renderer* renderer); + + /** + * @brief Load a model from a GLTF file. + * @param filename The path to the GLTF file. + * @return Pointer to the loaded model, or nullptr if loading failed. + */ + Model* LoadGLTF(const std::string& filename); + + /** + * @brief Load a model from a GLTF file with PBR materials. + * @param filename The path to the GLTF file. + * @param albedoMap The path to the albedo texture. + * @param normalMap The path to the normal texture. + * @param metallicRoughnessMap The path to the metallic-roughness texture. + * @param aoMap The path to the ambient occlusion texture. + * @param emissiveMap The path to the emissive texture. + * @return Pointer to the loaded model, or nullptr if loading failed. + */ + Model* LoadGLTFWithPBR(const std::string& filename, + const std::string& albedoMap, + const std::string& normalMap, + const std::string& metallicRoughnessMap, + const std::string& aoMap, + const std::string& emissiveMap); + + /** + * @brief Get a model by name. + * @param name The name of the model. + * @return Pointer to the model, or nullptr if not found. + */ + Model* GetModel(const std::string& name); + + /** + * @brief Create a new material with PBR properties. + * @param name The name of the material. + * @param albedo The albedo color. + * @param metallic The metallic factor. + * @param roughness The roughness factor. + * @param ao The ambient occlusion factor. + * @param emissive The emissive color. + * @return Pointer to the created material, or nullptr if creation failed. + */ + Material* CreatePBRMaterial(const std::string& name, + const glm::vec3& albedo, + float metallic, + float roughness, + float ao, + const glm::vec3& emissive); + +private: + // Reference to the renderer + Renderer* renderer = nullptr; + + // Loaded models + std::unordered_map> models; + + // Loaded materials + std::unordered_map> materials; + + /** + * @brief Parse a GLTF file. + * @param filename The path to the GLTF file. + * @param model The model to populate. + * @return True if parsing was successful, false otherwise. + */ + bool ParseGLTF(const std::string& filename, Model* model); + + /** + * @brief Load textures for a PBR material. + * @param material The material to populate. + * @param albedoMap The path to the albedo texture. + * @param normalMap The path to the normal texture. + * @param metallicRoughnessMap The path to the metallic-roughness texture. + * @param aoMap The path to the ambient occlusion texture. + * @param emissiveMap The path to the emissive texture. + * @return True if loading was successful, false otherwise. + */ + bool LoadPBRTextures(Material* material, + const std::string& albedoMap, + const std::string& normalMap, + const std::string& metallicRoughnessMap, + const std::string& aoMap, + const std::string& emissiveMap); +}; diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp new file mode 100644 index 00000000..ef2d3a14 --- /dev/null +++ b/attachments/simple_engine/physics_system.cpp @@ -0,0 +1,1151 @@ +#include "physics_system.h" +#include "entity.h" +#include "renderer.h" +#include +#include +#include +#include +#include + +// Concrete implementation of RigidBody +class ConcreteRigidBody : public RigidBody { +public: + ConcreteRigidBody(Entity* entity, CollisionShape shape, float mass) + : entity(entity), shape(shape), mass(mass) { + // Initialize with entity's transform if available + if (entity) { + // This would normally get the position, rotation, and scale from the entity's transform component + position = glm::vec3(0.0f); + rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + scale = glm::vec3(1.0f); + } + } + + ~ConcreteRigidBody() override = default; + + void SetPosition(const glm::vec3& position) override { + this->position = position; + std::cout << "Setting rigid body position to (" << position.x << ", " << position.y << ", " << position.z << ")" << std::endl; + } + + void SetRotation(const glm::quat& rotation) override { + this->rotation = rotation; + std::cout << "Setting rigid body rotation" << std::endl; + } + + void SetScale(const glm::vec3& scale) override { + this->scale = scale; + std::cout << "Setting rigid body scale to (" << scale.x << ", " << scale.y << ", " << scale.z << ")" << std::endl; + } + + void SetMass(float mass) override { + this->mass = mass; + std::cout << "Setting rigid body mass to " << mass << std::endl; + } + + void SetRestitution(float restitution) override { + this->restitution = restitution; + std::cout << "Setting rigid body restitution to " << restitution << std::endl; + } + + void SetFriction(float friction) override { + this->friction = friction; + std::cout << "Setting rigid body friction to " << friction << std::endl; + } + + void ApplyForce(const glm::vec3& force, const glm::vec3& localPosition) override { + std::cout << "Applying force (" << force.x << ", " << force.y << ", " << force.z << ") " + << "at local position (" << localPosition.x << ", " << localPosition.y << ", " << localPosition.z << ")" << std::endl; + + // In a real implementation, this would apply the force to the rigid body + linearVelocity += force / mass; + } + + void ApplyImpulse(const glm::vec3& impulse, const glm::vec3& localPosition) override { + std::cout << "Applying impulse (" << impulse.x << ", " << impulse.y << ", " << impulse.z << ") " + << "at local position (" << localPosition.x << ", " << localPosition.y << ", " << localPosition.z << ")" << std::endl; + + // In a real implementation, this would apply the impulse to the rigid body + linearVelocity += impulse / mass; + } + + void SetLinearVelocity(const glm::vec3& velocity) override { + linearVelocity = velocity; + std::cout << "Setting rigid body linear velocity to (" << velocity.x << ", " << velocity.y << ", " << velocity.z << ")" << std::endl; + } + + void SetAngularVelocity(const glm::vec3& velocity) override { + angularVelocity = velocity; + std::cout << "Setting rigid body angular velocity to (" << velocity.x << ", " << velocity.y << ", " << velocity.z << ")" << std::endl; + } + + glm::vec3 GetPosition() const override { + return position; + } + + glm::quat GetRotation() const override { + return rotation; + } + + glm::vec3 GetLinearVelocity() const override { + return linearVelocity; + } + + glm::vec3 GetAngularVelocity() const override { + return angularVelocity; + } + + void SetKinematic(bool kinematic) override { + this->kinematic = kinematic; + std::cout << "Setting rigid body kinematic to " << (kinematic ? "true" : "false") << std::endl; + } + + bool IsKinematic() const override { + return kinematic; + } + + Entity* GetEntity() const { + return entity; + } + + CollisionShape GetShape() const { + return shape; + } + + float GetMass() const { + return mass; + } + + float GetInverseMass() const { + return mass > 0.0f ? 1.0f / mass : 0.0f; + } + + float GetRestitution() const { + return restitution; + } + + float GetFriction() const { + return friction; + } + + void Update(float deltaTime, const glm::vec3& gravity) { + if (kinematic) { + return; + } + + // Apply gravity + linearVelocity += gravity * deltaTime; + + // Update position + position += linearVelocity * deltaTime; + + // Update rotation + glm::quat angularVelocityQuat(0.0f, angularVelocity.x, angularVelocity.y, angularVelocity.z); + rotation += 0.5f * deltaTime * angularVelocityQuat * rotation; + rotation = glm::normalize(rotation); + + // Update entity transform if available + if (entity) { + // This would normally set the position, rotation, and scale on the entity's transform component + } + } + +private: + Entity* entity = nullptr; + CollisionShape shape; + + glm::vec3 position = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + glm::vec3 scale = glm::vec3(1.0f); + + glm::vec3 linearVelocity = glm::vec3(0.0f); + glm::vec3 angularVelocity = glm::vec3(0.0f); + + float mass = 1.0f; + float restitution = 0.5f; + float friction = 0.5f; + + bool kinematic = false; + + friend class PhysicsSystem; +}; + +PhysicsSystem::PhysicsSystem() { + // Constructor implementation +} + +PhysicsSystem::~PhysicsSystem() { + // Destructor implementation + if (initialized && gpuAccelerationEnabled) { + CleanupVulkanResources(); + } + rigidBodies.clear(); +} + +bool PhysicsSystem::Initialize() { + // This is a placeholder implementation + // In a real implementation, this would initialize the physics engine + + std::cout << "Initializing physics system" << std::endl; + + // Initialize Vulkan resources if GPU acceleration is enabled and renderer is set + if (gpuAccelerationEnabled && renderer) { + if (!InitializeVulkanResources()) { + std::cerr << "Failed to initialize Vulkan resources for physics system" << std::endl; + gpuAccelerationEnabled = false; + } + } + + initialized = true; + return true; +} + +void PhysicsSystem::Update(float deltaTime) { + // If GPU acceleration is enabled and we have a renderer, use the GPU + if (initialized && gpuAccelerationEnabled && renderer && rigidBodies.size() <= maxGPUObjects) { + SimulatePhysicsOnGPU(deltaTime); + } else { + // Fall back to CPU physics + // Update all rigid bodies + for (auto& rigidBody : rigidBodies) { + auto concreteRigidBody = static_cast(rigidBody.get()); + concreteRigidBody->Update(deltaTime, gravity); + } + + // Perform collision detection and resolution + // This would be a complex algorithm in a real implementation + } +} + +RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, float mass) { + // Create a new rigid body + auto rigidBody = std::make_unique(entity, shape, mass); + + // Store the rigid body + RigidBody* rigidBodyPtr = rigidBody.get(); + rigidBodies.push_back(std::move(rigidBody)); + + std::cout << "Rigid body created for entity: " << (entity ? entity->GetName() : "null") << std::endl; + return rigidBodyPtr; +} + +bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { + // Find the rigid body in the vector + auto it = std::find_if(rigidBodies.begin(), rigidBodies.end(), + [rigidBody](const std::unique_ptr& rb) { + return rb.get() == rigidBody; + }); + + if (it != rigidBodies.end()) { + // Remove the rigid body + rigidBodies.erase(it); + + std::cout << "Rigid body removed" << std::endl; + return true; + } + + std::cerr << "PhysicsSystem::RemoveRigidBody: Rigid body not found" << std::endl; + return false; +} + +void PhysicsSystem::SetGravity(const glm::vec3& gravity) { + this->gravity = gravity; + + std::cout << "Setting gravity to (" << gravity.x << ", " << gravity.y << ", " << gravity.z << ")" << std::endl; +} + +glm::vec3 PhysicsSystem::GetGravity() const { + return gravity; +} + +bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, + glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity) { + std::cout << "Performing raycast from (" << origin.x << ", " << origin.y << ", " << origin.z << ") " + << "in direction (" << direction.x << ", " << direction.y << ", " << direction.z << ") " + << "with max distance " << maxDistance << std::endl; + + // Normalize the direction vector + glm::vec3 normalizedDirection = glm::normalize(direction); + + // Variables to track the closest hit + float closestHitDistance = maxDistance; + bool hitFound = false; + glm::vec3 closestHitPosition; + glm::vec3 closestHitNormal; + Entity* closestHitEntity = nullptr; + + // Check each rigid body for intersection + for (const auto& rigidBody : rigidBodies) { + auto concreteRigidBody = static_cast(rigidBody.get()); + Entity* entity = concreteRigidBody->GetEntity(); + + // Skip if the entity is null + if (!entity) { + continue; + } + + // Get the position and shape of the rigid body + glm::vec3 position = concreteRigidBody->GetPosition(); + CollisionShape shape = concreteRigidBody->GetShape(); + + // Variables for hit detection + float hitDistance = 0.0f; + glm::vec3 localHitPosition; + glm::vec3 localHitNormal; + bool hit = false; + + // Check for intersection based on the shape + switch (shape) { + case CollisionShape::Sphere: { + // Sphere intersection test + float radius = 0.5f; // Default radius + + // Calculate coefficients for quadratic equation + glm::vec3 oc = origin - position; + float a = glm::dot(normalizedDirection, normalizedDirection); + float b = 2.0f * glm::dot(oc, normalizedDirection); + float c = glm::dot(oc, oc) - radius * radius; + float discriminant = b * b - 4 * a * c; + + if (discriminant >= 0) { + // Calculate intersection distance + float t = (-b - std::sqrt(discriminant)) / (2.0f * a); + + // Check if intersection is within range + if (t > 0 && t < closestHitDistance) { + hitDistance = t; + localHitPosition = origin + normalizedDirection * t; + localHitNormal = glm::normalize(localHitPosition - position); + hit = true; + } + } + break; + } + case CollisionShape::Box: { + // Box intersection test (AABB) + glm::vec3 halfExtents(0.5f, 0.5f, 0.5f); // Default box size + + // Calculate min and max bounds of the box + glm::vec3 boxMin = position - halfExtents; + glm::vec3 boxMax = position + halfExtents; + + // Calculate intersection with each slab + float tmin = -INFINITY, tmax = INFINITY; + + for (int i = 0; i < 3; i++) { + if (std::abs(normalizedDirection[i]) < 0.0001f) { + // Ray is parallel to slab, check if origin is within slab + if (origin[i] < boxMin[i] || origin[i] > boxMax[i]) { + // No intersection + hit = false; + break; + } + } else { + // Calculate intersection distances + float ood = 1.0f / normalizedDirection[i]; + float t1 = (boxMin[i] - origin[i]) * ood; + float t2 = (boxMax[i] - origin[i]) * ood; + + // Ensure t1 <= t2 + if (t1 > t2) { + std::swap(t1, t2); + } + + // Update tmin and tmax + tmin = std::max(tmin, t1); + tmax = std::min(tmax, t2); + + if (tmin > tmax) { + // No intersection + hit = false; + break; + } + } + } + + // Check if intersection is within range + if (tmin > 0 && tmin < closestHitDistance) { + hitDistance = tmin; + localHitPosition = origin + normalizedDirection * tmin; + + // Calculate normal based on which face was hit + glm::vec3 center = position; + glm::vec3 d = localHitPosition - center; + float bias = 1.00001f; // Small bias to ensure we get the correct face + + localHitNormal = glm::vec3(0.0f); + if (d.x > halfExtents.x * bias) localHitNormal = glm::vec3(1, 0, 0); + else if (d.x < -halfExtents.x * bias) localHitNormal = glm::vec3(-1, 0, 0); + else if (d.y > halfExtents.y * bias) localHitNormal = glm::vec3(0, 1, 0); + else if (d.y < -halfExtents.y * bias) localHitNormal = glm::vec3(0, -1, 0); + else if (d.z > halfExtents.z * bias) localHitNormal = glm::vec3(0, 0, 1); + else if (d.z < -halfExtents.z * bias) localHitNormal = glm::vec3(0, 0, -1); + + hit = true; + } + break; + } + case CollisionShape::Capsule: { + // Capsule intersection test + // Simplified as a line segment with spheres at each end + float radius = 0.5f; // Default radius + float halfHeight = 0.5f; // Default half-height + + // Define capsule line segment + glm::vec3 capsuleA = position + glm::vec3(0, -halfHeight, 0); + glm::vec3 capsuleB = position + glm::vec3(0, halfHeight, 0); + + // Calculate closest point on line segment + glm::vec3 ab = capsuleB - capsuleA; + glm::vec3 ao = origin - capsuleA; + + float t = glm::dot(ao, ab) / glm::dot(ab, ab); + t = glm::clamp(t, 0.0f, 1.0f); + + glm::vec3 closestPoint = capsuleA + ab * t; + + // Sphere intersection test with closest point + glm::vec3 oc = origin - closestPoint; + float a = glm::dot(normalizedDirection, normalizedDirection); + float b = 2.0f * glm::dot(oc, normalizedDirection); + float c = glm::dot(oc, oc) - radius * radius; + float discriminant = b * b - 4 * a * c; + + if (discriminant >= 0) { + // Calculate intersection distance + float t = (-b - std::sqrt(discriminant)) / (2.0f * a); + + // Check if intersection is within range + if (t > 0 && t < closestHitDistance) { + hitDistance = t; + localHitPosition = origin + normalizedDirection * t; + localHitNormal = glm::normalize(localHitPosition - closestPoint); + hit = true; + } + } + break; + } + case CollisionShape::Mesh: { + // Mesh intersection test + // In a real implementation, this would perform intersection tests with each triangle in the mesh + // For simplicity, we'll just simulate a hit with a sphere + + float radius = 0.5f; // Default radius + + // Calculate coefficients for quadratic equation + glm::vec3 oc = origin - position; + float a = glm::dot(normalizedDirection, normalizedDirection); + float b = 2.0f * glm::dot(oc, normalizedDirection); + float c = glm::dot(oc, oc) - radius * radius; + float discriminant = b * b - 4 * a * c; + + if (discriminant >= 0) { + // Calculate intersection distance + float t = (-b - std::sqrt(discriminant)) / (2.0f * a); + + // Check if intersection is within range + if (t > 0 && t < closestHitDistance) { + hitDistance = t; + localHitPosition = origin + normalizedDirection * t; + localHitNormal = glm::normalize(localHitPosition - position); + hit = true; + } + } + break; + } + default: + break; + } + + // Update closest hit if a hit was found + if (hit && hitDistance < closestHitDistance) { + closestHitDistance = hitDistance; + closestHitPosition = localHitPosition; + closestHitNormal = localHitNormal; + closestHitEntity = entity; + hitFound = true; + } + } + + // Set output parameters if a hit was found + if (hitFound) { + if (hitPosition) { + *hitPosition = closestHitPosition; + } + + if (hitNormal) { + *hitNormal = closestHitNormal; + } + + if (hitEntity) { + *hitEntity = closestHitEntity; + } + + std::cout << "Hit found at distance " << closestHitDistance << std::endl; + std::cout << "Hit position: (" << closestHitPosition.x << ", " << closestHitPosition.y << ", " << closestHitPosition.z << ")" << std::endl; + std::cout << "Hit normal: (" << closestHitNormal.x << ", " << closestHitNormal.y << ", " << closestHitNormal.z << ")" << std::endl; + std::cout << "Hit entity: " << (closestHitEntity ? closestHitEntity->GetName() : "null") << std::endl; + } + + return hitFound; +} + +// Helper function to read a shader file +static std::vector readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + size_t fileSize = (size_t)file.tellg(); + std::vector buffer(fileSize); + + file.seekg(0); + file.read(buffer.data(), fileSize); + file.close(); + + return buffer; +} + +// Helper function to create a shader module +static vk::raii::ShaderModule createShaderModule(const vk::raii::Device& device, const std::vector& code) { + vk::ShaderModuleCreateInfo createInfo; + createInfo.codeSize = code.size(); + createInfo.pCode = reinterpret_cast(code.data()); + + try { + return vk::raii::ShaderModule(device, createInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create shader module: " + std::string(e.what())); + } +} + +bool PhysicsSystem::InitializeVulkanResources() { + if (!renderer) { + std::cerr << "Renderer is not set" << std::endl; + return false; + } + + vk::Device device = renderer->GetDevice(); + if (!device) { + std::cerr << "Vulkan device is not valid" << std::endl; + return false; + } + + try { + // Create shader modules + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); + + std::vector integrateShaderCode = readFile("shaders/physics.spv"); + vulkanResources.integrateShaderModule = createShaderModule(raiiDevice, integrateShaderCode); + + std::vector broadPhaseShaderCode = readFile("shaders/physics.spv"); + vulkanResources.broadPhaseShaderModule = createShaderModule(raiiDevice, broadPhaseShaderCode); + + std::vector narrowPhaseShaderCode = readFile("shaders/physics.spv"); + vulkanResources.narrowPhaseShaderModule = createShaderModule(raiiDevice, narrowPhaseShaderCode); + + std::vector resolveShaderCode = readFile("shaders/physics.spv"); + vulkanResources.resolveShaderModule = createShaderModule(raiiDevice, resolveShaderCode); + + // Create descriptor set layout + std::array bindings = { + // Physics data buffer + vk::DescriptorSetLayoutBinding( + 0, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Collision data buffer + vk::DescriptorSetLayoutBinding( + 1, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Pair buffer + vk::DescriptorSetLayoutBinding( + 2, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Counter buffer + vk::DescriptorSetLayoutBinding( + 3, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Parameters buffer + vk::DescriptorSetLayoutBinding( + 4, // binding + vk::DescriptorType::eUniformBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ) + }; + + vk::DescriptorSetLayoutCreateInfo layoutInfo; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + try { + vulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(raiiDevice, layoutInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create descriptor set layout: " + std::string(e.what())); + } + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo; + pipelineLayoutInfo.setLayoutCount = 1; + vk::DescriptorSetLayout descriptorSetLayout = *vulkanResources.descriptorSetLayout; + pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; + + try { + vulkanResources.pipelineLayout = vk::raii::PipelineLayout(raiiDevice, pipelineLayoutInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create pipeline layout: " + std::string(e.what())); + } + + // Create compute pipelines + vk::ComputePipelineCreateInfo pipelineInfo; + pipelineInfo.layout = *vulkanResources.pipelineLayout; + pipelineInfo.basePipelineHandle = nullptr; + + // Integrate pipeline + vk::PipelineShaderStageCreateInfo integrateStageInfo; + integrateStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + integrateStageInfo.module = *vulkanResources.integrateShaderModule; + integrateStageInfo.pName = "IntegrateCS"; + pipelineInfo.stage = integrateStageInfo; + + try { + vulkanResources.integratePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create integrate compute pipeline: " + std::string(e.what())); + } + + // Broad phase pipeline + vk::PipelineShaderStageCreateInfo broadPhaseStageInfo; + broadPhaseStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + broadPhaseStageInfo.module = *vulkanResources.broadPhaseShaderModule; + broadPhaseStageInfo.pName = "BroadPhaseCS"; + pipelineInfo.stage = broadPhaseStageInfo; + + try { + vulkanResources.broadPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create broad phase compute pipeline: " + std::string(e.what())); + } + + // Narrow phase pipeline + vk::PipelineShaderStageCreateInfo narrowPhaseStageInfo; + narrowPhaseStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + narrowPhaseStageInfo.module = *vulkanResources.narrowPhaseShaderModule; + narrowPhaseStageInfo.pName = "NarrowPhaseCS"; + pipelineInfo.stage = narrowPhaseStageInfo; + + try { + vulkanResources.narrowPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create narrow phase compute pipeline: " + std::string(e.what())); + } + + // Resolve pipeline + vk::PipelineShaderStageCreateInfo resolveStageInfo; + resolveStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + resolveStageInfo.module = *vulkanResources.resolveShaderModule; + resolveStageInfo.pName = "ResolveCS"; + pipelineInfo.stage = resolveStageInfo; + + try { + vulkanResources.resolvePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create resolve compute pipeline: " + std::string(e.what())); + } + + // Create buffers + vk::DeviceSize physicsBufferSize = sizeof(GPUPhysicsData) * maxGPUObjects; + vk::DeviceSize collisionBufferSize = sizeof(GPUCollisionData) * maxGPUCollisions; + vk::DeviceSize pairBufferSize = sizeof(uint32_t) * 2 * maxGPUCollisions; + vk::DeviceSize counterBufferSize = sizeof(uint32_t) * 2; + vk::DeviceSize paramsBufferSize = sizeof(PhysicsParams); + + // Create physics buffer + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = physicsBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + try { + vulkanResources.physicsBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.physicsBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + vulkanResources.physicsBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.physicsBuffer.bindMemory(*vulkanResources.physicsBufferMemory, 0); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create physics buffer: " + std::string(e.what())); + } + + // Create collision buffer + bufferInfo.size = collisionBufferSize; + try { + vulkanResources.collisionBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.collisionBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + vulkanResources.collisionBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.collisionBuffer.bindMemory(*vulkanResources.collisionBufferMemory, 0); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create collision buffer: " + std::string(e.what())); + } + + // Create pair buffer + bufferInfo.size = pairBufferSize; + try { + vulkanResources.pairBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.pairBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + vulkanResources.pairBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.pairBuffer.bindMemory(*vulkanResources.pairBufferMemory, 0); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create pair buffer: " + std::string(e.what())); + } + + // Create counter buffer + bufferInfo.size = counterBufferSize; + try { + vulkanResources.counterBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.counterBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + vulkanResources.counterBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.counterBuffer.bindMemory(*vulkanResources.counterBufferMemory, 0); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create counter buffer: " + std::string(e.what())); + } + + // Create params buffer + bufferInfo.size = paramsBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; + try { + vulkanResources.paramsBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.paramsBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + vulkanResources.paramsBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.paramsBuffer.bindMemory(*vulkanResources.paramsBufferMemory, 0); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create params buffer: " + std::string(e.what())); + } + + // Initialize counter buffer + uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count + void* data = vulkanResources.counterBufferMemory.mapMemory(0, sizeof(initialCounters)); + memcpy(data, initialCounters, sizeof(initialCounters)); + vulkanResources.counterBufferMemory.unmapMemory(); + + // Create descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 4), + vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1) + }; + + vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.poolSizeCount = static_cast(poolSizes.size()); + poolInfo.pPoolSizes = poolSizes.data(); + poolInfo.maxSets = 1; + + try { + vulkanResources.descriptorPool = vk::raii::DescriptorPool(raiiDevice, poolInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create descriptor pool: " + std::string(e.what())); + } + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo descriptorSetAllocInfo; + descriptorSetAllocInfo.descriptorPool = *vulkanResources.descriptorPool; + descriptorSetAllocInfo.descriptorSetCount = 1; + vk::DescriptorSetLayout descriptorSetLayoutRef = *vulkanResources.descriptorSetLayout; + descriptorSetAllocInfo.pSetLayouts = &descriptorSetLayoutRef; + + try { + std::vector raiiDescriptorSets = raiiDevice.allocateDescriptorSets(descriptorSetAllocInfo); + vulkanResources.descriptorSets.resize(raiiDescriptorSets.size()); + for (size_t i = 0; i < raiiDescriptorSets.size(); ++i) { + vulkanResources.descriptorSets[i] = *raiiDescriptorSets[i]; + } + } catch (const std::exception& e) { + throw std::runtime_error("Failed to allocate descriptor sets: " + std::string(e.what())); + } + + // Update descriptor sets + vk::DescriptorBufferInfo physicsBufferInfo; + physicsBufferInfo.buffer = *vulkanResources.physicsBuffer; + physicsBufferInfo.offset = 0; + physicsBufferInfo.range = physicsBufferSize; + + vk::DescriptorBufferInfo collisionBufferInfo; + collisionBufferInfo.buffer = *vulkanResources.collisionBuffer; + collisionBufferInfo.offset = 0; + collisionBufferInfo.range = collisionBufferSize; + + vk::DescriptorBufferInfo pairBufferInfo; + pairBufferInfo.buffer = *vulkanResources.pairBuffer; + pairBufferInfo.offset = 0; + pairBufferInfo.range = pairBufferSize; + + vk::DescriptorBufferInfo counterBufferInfo; + counterBufferInfo.buffer = *vulkanResources.counterBuffer; + counterBufferInfo.offset = 0; + counterBufferInfo.range = counterBufferSize; + + vk::DescriptorBufferInfo paramsBufferInfo; + paramsBufferInfo.buffer = *vulkanResources.paramsBuffer; + paramsBufferInfo.offset = 0; + paramsBufferInfo.range = paramsBufferSize; + + std::array descriptorWrites; + + // Physics buffer + descriptorWrites[0].setDstSet(vulkanResources.descriptorSets[0]) + .setDstBinding(0) + .setDstArrayElement(0) + .setDescriptorCount(1) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setPBufferInfo(&physicsBufferInfo); + + // Collision buffer + descriptorWrites[1].setDstSet(vulkanResources.descriptorSets[0]) + .setDstBinding(1) + .setDstArrayElement(0) + .setDescriptorCount(1) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setPBufferInfo(&collisionBufferInfo); + + // Pair buffer + descriptorWrites[2].setDstSet(vulkanResources.descriptorSets[0]) + .setDstBinding(2) + .setDstArrayElement(0) + .setDescriptorCount(1) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setPBufferInfo(&pairBufferInfo); + + // Counter buffer + descriptorWrites[3].setDstSet(vulkanResources.descriptorSets[0]) + .setDstBinding(3) + .setDstArrayElement(0) + .setDescriptorCount(1) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setPBufferInfo(&counterBufferInfo); + + // Params buffer + descriptorWrites[4].setDstSet(vulkanResources.descriptorSets[0]) + .setDstBinding(4) + .setDstArrayElement(0) + .setDescriptorCount(1) + .setDescriptorType(vk::DescriptorType::eUniformBuffer) + .setPBufferInfo(¶msBufferInfo); + + raiiDevice.updateDescriptorSets(descriptorWrites, nullptr); + + // Create command pool + vk::CommandPoolCreateInfo commandPoolInfo; + commandPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; + commandPoolInfo.queueFamilyIndex = 0; // Assuming compute queue family index is 0 + + try { + vulkanResources.commandPool = vk::raii::CommandPool(raiiDevice, commandPoolInfo); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create command pool: " + std::string(e.what())); + } + + // Allocate command buffer + vk::CommandBufferAllocateInfo commandBufferInfo; + commandBufferInfo.commandPool = *vulkanResources.commandPool; + commandBufferInfo.level = vk::CommandBufferLevel::ePrimary; + commandBufferInfo.commandBufferCount = 1; + + try { + std::vector commandBuffers = raiiDevice.allocateCommandBuffers(commandBufferInfo); + vulkanResources.commandBuffer = std::move(commandBuffers.front()); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to allocate command buffer: " + std::string(e.what())); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Error initializing Vulkan resources: " << e.what() << std::endl; + CleanupVulkanResources(); + return false; + } +} + +void PhysicsSystem::CleanupVulkanResources() { + if (!renderer) { + return; + } + + // Wait for the device to be idle before cleaning up + renderer->WaitIdle(); + + // With RAII, we just need to set the resources to nullptr + // The destructors will handle the cleanup + vulkanResources.commandBuffer = nullptr; + vulkanResources.commandPool = nullptr; + vulkanResources.paramsBuffer = nullptr; + vulkanResources.paramsBufferMemory = nullptr; + vulkanResources.counterBuffer = nullptr; + vulkanResources.counterBufferMemory = nullptr; + vulkanResources.pairBuffer = nullptr; + vulkanResources.pairBufferMemory = nullptr; + vulkanResources.collisionBuffer = nullptr; + vulkanResources.collisionBufferMemory = nullptr; + vulkanResources.physicsBuffer = nullptr; + vulkanResources.physicsBufferMemory = nullptr; + vulkanResources.descriptorPool = nullptr; + vulkanResources.resolvePipeline = nullptr; + vulkanResources.narrowPhasePipeline = nullptr; + vulkanResources.broadPhasePipeline = nullptr; + vulkanResources.integratePipeline = nullptr; + vulkanResources.pipelineLayout = nullptr; + vulkanResources.descriptorSetLayout = nullptr; + vulkanResources.resolveShaderModule = nullptr; + vulkanResources.narrowPhaseShaderModule = nullptr; + vulkanResources.broadPhaseShaderModule = nullptr; + vulkanResources.integrateShaderModule = nullptr; +} + +void PhysicsSystem::UpdateGPUPhysicsData() { + if (!renderer) { + return; + } + + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); + + + // Map the physics buffer + void* data = vulkanResources.physicsBufferMemory.mapMemory(0, sizeof(GPUPhysicsData) * rigidBodies.size()); + + // Copy physics data to the buffer + GPUPhysicsData* gpuData = static_cast(data); + for (size_t i = 0; i < rigidBodies.size(); i++) { + auto concreteRigidBody = static_cast(rigidBodies[i].get()); + + gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); + gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, + concreteRigidBody->GetRotation().z, concreteRigidBody->GetRotation().w); + gpuData[i].linearVelocity = glm::vec4(concreteRigidBody->GetLinearVelocity(), concreteRigidBody->GetRestitution()); + gpuData[i].angularVelocity = glm::vec4(concreteRigidBody->GetAngularVelocity(), concreteRigidBody->GetFriction()); + gpuData[i].force = glm::vec4(glm::vec3(0.0f), concreteRigidBody->IsKinematic() ? 1.0f : 0.0f); + gpuData[i].torque = glm::vec4(glm::vec3(0.0f), 1.0f); // Always use gravity + + // Set collider data based on collider type + CollisionShape shape = concreteRigidBody->GetShape(); + switch (shape) { + case CollisionShape::Sphere: + gpuData[i].colliderData = glm::vec4(0.5f, 0.0f, 0.0f, static_cast(0)); // 0 = Sphere + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + case CollisionShape::Box: + gpuData[i].colliderData = glm::vec4(0.5f, 0.5f, 0.5f, static_cast(1)); // 1 = Box + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + default: + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); // Invalid + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + } + } + + vulkanResources.physicsBufferMemory.unmapMemory(); + + // Reset counters + uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count + data = vulkanResources.counterBufferMemory.mapMemory(0, sizeof(initialCounters)); + memcpy(data, initialCounters, sizeof(initialCounters)); + vulkanResources.counterBufferMemory.unmapMemory(); + + // Update params buffer + PhysicsParams params; + params.deltaTime = 1.0f / 60.0f; // Fixed time step + params.gravity = gravity; + params.numBodies = static_cast(rigidBodies.size()); + params.maxCollisions = maxGPUCollisions; + + data = vulkanResources.paramsBufferMemory.mapMemory(0, sizeof(PhysicsParams)); + memcpy(data, ¶ms, sizeof(PhysicsParams)); + vulkanResources.paramsBufferMemory.unmapMemory(); +} + +void PhysicsSystem::ReadbackGPUPhysicsData() { + if (!renderer) { + return; + } + + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); + + + // Map the physics buffer + void* data = vulkanResources.physicsBufferMemory.mapMemory(0, sizeof(GPUPhysicsData) * rigidBodies.size()); + + // Copy physics data from the buffer + GPUPhysicsData* gpuData = static_cast(data); + for (size_t i = 0; i < rigidBodies.size(); i++) { + auto concreteRigidBody = static_cast(rigidBodies[i].get()); + + // Skip kinematic bodies + if (concreteRigidBody->IsKinematic()) { + continue; + } + + concreteRigidBody->SetPosition(glm::vec3(gpuData[i].position)); + concreteRigidBody->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, + gpuData[i].rotation.y, gpuData[i].rotation.z)); + concreteRigidBody->SetLinearVelocity(glm::vec3(gpuData[i].linearVelocity)); + concreteRigidBody->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); + } + + vulkanResources.physicsBufferMemory.unmapMemory(); +} + +void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { + if (!renderer) { + return; + } + + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); + + + // Update physics data on the GPU + UpdateGPUPhysicsData(); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo; + beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit; + + vulkanResources.commandBuffer.begin(beginInfo); + + // Bind descriptor set + vulkanResources.commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + *vulkanResources.pipelineLayout, + 0, + vulkanResources.descriptorSets, + nullptr + ); + + // Step 1: Integrate forces and velocities + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.integratePipeline); + vulkanResources.commandBuffer.dispatch((rigidBodies.size() + 63) / 64, 1, 1); + + // Memory barrier to ensure integration is complete before collision detection + vk::MemoryBarrier memoryBarrier; + memoryBarrier.srcAccessMask = vk::AccessFlagBits::eShaderWrite; + memoryBarrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + memoryBarrier, + nullptr, + nullptr + ); + + // Step 2: Broad-phase collision detection + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.broadPhasePipeline); + // Each thread checks one pair of objects + uint32_t numPairs = (rigidBodies.size() * (rigidBodies.size() - 1)) / 2; + vulkanResources.commandBuffer.dispatch((numPairs + 63) / 64, 1, 1); + + // Memory barrier to ensure broad phase is complete before narrow phase + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + memoryBarrier, + nullptr, + nullptr + ); + + // Step 3: Narrow-phase collision detection + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.narrowPhasePipeline); + // We don't know how many pairs were generated, so we use a conservative estimate + vulkanResources.commandBuffer.dispatch((maxGPUCollisions + 63) / 64, 1, 1); + + // Memory barrier to ensure narrow phase is complete before resolution + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + memoryBarrier, + nullptr, + nullptr + ); + + // Step 4: Collision resolution + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.resolvePipeline); + // We don't know how many collisions were detected, so we use a conservative estimate + vulkanResources.commandBuffer.dispatch((maxGPUCollisions + 63) / 64, 1, 1); + + // End command buffer + vulkanResources.commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo; + submitInfo.commandBufferCount = 1; + vk::CommandBuffer cmdBuffer = *vulkanResources.commandBuffer; + submitInfo.pCommandBuffers = &cmdBuffer; + + vk::Queue computeQueue = renderer->GetComputeQueue(); + computeQueue.submit(submitInfo, nullptr); + computeQueue.waitIdle(); + + // Read back physics data from the GPU + ReadbackGPUPhysicsData(); +} diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h new file mode 100644 index 00000000..ffc428d4 --- /dev/null +++ b/attachments/simple_engine/physics_system.h @@ -0,0 +1,339 @@ +#pragma once + +#include +#include +#include +#include +#include +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include + +class Entity; +class Renderer; + +/** + * @brief Enum for different collision shapes. + */ +enum class CollisionShape { + Box, + Sphere, + Capsule, + Mesh +}; + +/** + * @brief Class representing a rigid body for physics simulation. + */ +class RigidBody { +public: + /** + * @brief Default constructor. + */ + RigidBody() = default; + + /** + * @brief Destructor for proper cleanup. + */ + virtual ~RigidBody() = default; + + /** + * @brief Set the position of the rigid body. + * @param position The position. + */ + virtual void SetPosition(const glm::vec3& position) = 0; + + /** + * @brief Set the rotation of the rigid body. + * @param rotation The rotation quaternion. + */ + virtual void SetRotation(const glm::quat& rotation) = 0; + + /** + * @brief Set the scale of the rigid body. + * @param scale The scale. + */ + virtual void SetScale(const glm::vec3& scale) = 0; + + /** + * @brief Set the mass of the rigid body. + * @param mass The mass. + */ + virtual void SetMass(float mass) = 0; + + /** + * @brief Set the restitution (bounciness) of the rigid body. + * @param restitution The restitution (0.0f to 1.0f). + */ + virtual void SetRestitution(float restitution) = 0; + + /** + * @brief Set the friction of the rigid body. + * @param friction The friction (0.0f to 1.0f). + */ + virtual void SetFriction(float friction) = 0; + + /** + * @brief Apply a force to the rigid body. + * @param force The force vector. + * @param localPosition The local position to apply the force at. + */ + virtual void ApplyForce(const glm::vec3& force, const glm::vec3& localPosition = glm::vec3(0.0f)) = 0; + + /** + * @brief Apply an impulse to the rigid body. + * @param impulse The impulse vector. + * @param localPosition The local position to apply the impulse at. + */ + virtual void ApplyImpulse(const glm::vec3& impulse, const glm::vec3& localPosition = glm::vec3(0.0f)) = 0; + + /** + * @brief Set the linear velocity of the rigid body. + * @param velocity The linear velocity. + */ + virtual void SetLinearVelocity(const glm::vec3& velocity) = 0; + + /** + * @brief Set the angular velocity of the rigid body. + * @param velocity The angular velocity. + */ + virtual void SetAngularVelocity(const glm::vec3& velocity) = 0; + + /** + * @brief Get the position of the rigid body. + * @return The position. + */ + virtual glm::vec3 GetPosition() const = 0; + + /** + * @brief Get the rotation of the rigid body. + * @return The rotation quaternion. + */ + virtual glm::quat GetRotation() const = 0; + + /** + * @brief Get the linear velocity of the rigid body. + * @return The linear velocity. + */ + virtual glm::vec3 GetLinearVelocity() const = 0; + + /** + * @brief Get the angular velocity of the rigid body. + * @return The angular velocity. + */ + virtual glm::vec3 GetAngularVelocity() const = 0; + + /** + * @brief Set whether the rigid body is kinematic. + * @param kinematic Whether the rigid body is kinematic. + */ + virtual void SetKinematic(bool kinematic) = 0; + + /** + * @brief Check if the rigid body is kinematic. + * @return True if kinematic, false otherwise. + */ + virtual bool IsKinematic() const = 0; +}; + +/** + * @brief Structure for GPU physics data. + */ +struct GPUPhysicsData { + glm::vec4 position; // xyz = position, w = inverse mass + glm::vec4 rotation; // quaternion + glm::vec4 linearVelocity; // xyz = velocity, w = restitution + glm::vec4 angularVelocity; // xyz = angular velocity, w = friction + glm::vec4 force; // xyz = force, w = is kinematic (0 or 1) + glm::vec4 torque; // xyz = torque, w = use gravity (0 or 1) + glm::vec4 colliderData; // type-specific data (e.g., radius for spheres) + glm::vec4 colliderData2; // additional collider data (e.g., box half extents) +}; + +/** + * @brief Structure for GPU collision data. + */ +struct GPUCollisionData { + uint32_t bodyA; + uint32_t bodyB; + glm::vec4 contactNormal; // xyz = normal, w = penetration depth + glm::vec4 contactPoint; // xyz = contact point, w = unused +}; + +/** + * @brief Structure for physics simulation parameters. + */ +struct PhysicsParams { + float deltaTime; // Time step + glm::vec3 gravity; // Gravity vector + uint32_t numBodies; // Number of rigid bodies + uint32_t maxCollisions; // Maximum number of collisions +}; + +/** + * @brief Class for managing physics simulation. + * + * This class implements the physics system as described in the Subsystems chapter: + * @see en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc + * @see en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc + */ +class PhysicsSystem { +public: + /** + * @brief Default constructor. + */ + PhysicsSystem(); + + /** + * @brief Destructor for proper cleanup. + */ + ~PhysicsSystem(); + + /** + * @brief Initialize the physics system. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(); + + /** + * @brief Update the physics system. + * @param deltaTime The time elapsed since the last update. + */ + void Update(float deltaTime); + + /** + * @brief Create a rigid body. + * @param entity The entity to attach the rigid body to. + * @param shape The collision shape. + * @param mass The mass. + * @return Pointer to the created rigid body, or nullptr if creation failed. + */ + RigidBody* CreateRigidBody(Entity* entity, CollisionShape shape, float mass); + + /** + * @brief Remove a rigid body. + * @param rigidBody The rigid body to remove. + * @return True if removal was successful, false otherwise. + */ + bool RemoveRigidBody(RigidBody* rigidBody); + + /** + * @brief Set the gravity of the physics world. + * @param gravity The gravity vector. + */ + void SetGravity(const glm::vec3& gravity); + + /** + * @brief Get the gravity of the physics world. + * @return The gravity vector. + */ + glm::vec3 GetGravity() const; + + /** + * @brief Perform a raycast. + * @param origin The origin of the ray. + * @param direction The direction of the ray. + * @param maxDistance The maximum distance of the ray. + * @param hitPosition Output parameter for the hit position. + * @param hitNormal Output parameter for the hit normal. + * @param hitEntity Output parameter for the hit entity. + * @return True if the ray hit something, false otherwise. + */ + bool Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, + glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity); + + /** + * @brief Enable or disable GPU acceleration. + * @param enabled Whether GPU acceleration is enabled. + */ + void SetGPUAccelerationEnabled(bool enabled) { gpuAccelerationEnabled = enabled; } + + /** + * @brief Check if GPU acceleration is enabled. + * @return True if GPU acceleration is enabled, false otherwise. + */ + bool IsGPUAccelerationEnabled() const { return gpuAccelerationEnabled; } + + /** + * @brief Set the maximum number of objects that can be simulated on the GPU. + * @param maxObjects The maximum number of objects. + */ + void SetMaxGPUObjects(uint32_t maxObjects) { maxGPUObjects = maxObjects; } + + /** + * @brief Set the renderer to use for GPU acceleration. + * @param renderer The renderer. + */ + void SetRenderer(Renderer* renderer) { this->renderer = renderer; } + +private: + // Rigid bodies + std::vector> rigidBodies; + + // Gravity + glm::vec3 gravity = glm::vec3(0.0f, -9.81f, 0.0f); + + // Whether the physics system is initialized + bool initialized = false; + + // GPU acceleration + bool gpuAccelerationEnabled = false; + uint32_t maxGPUObjects = 1024; + uint32_t maxGPUCollisions = 4096; + Renderer* renderer = nullptr; + + // Vulkan resources for physics simulation + struct VulkanResources { + // Shader modules + vk::raii::ShaderModule integrateShaderModule = nullptr; + vk::raii::ShaderModule broadPhaseShaderModule = nullptr; + vk::raii::ShaderModule narrowPhaseShaderModule = nullptr; + vk::raii::ShaderModule resolveShaderModule = nullptr; + + // Pipeline layouts and compute pipelines + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline integratePipeline = nullptr; + vk::raii::Pipeline broadPhasePipeline = nullptr; + vk::raii::Pipeline narrowPhasePipeline = nullptr; + vk::raii::Pipeline resolvePipeline = nullptr; + + // Descriptor pool and sets + vk::raii::DescriptorPool descriptorPool = nullptr; + std::vector descriptorSets; + + // Buffers for physics data + vk::raii::Buffer physicsBuffer = nullptr; + vk::raii::DeviceMemory physicsBufferMemory = nullptr; + vk::raii::Buffer collisionBuffer = nullptr; + vk::raii::DeviceMemory collisionBufferMemory = nullptr; + vk::raii::Buffer pairBuffer = nullptr; + vk::raii::DeviceMemory pairBufferMemory = nullptr; + vk::raii::Buffer counterBuffer = nullptr; + vk::raii::DeviceMemory counterBufferMemory = nullptr; + vk::raii::Buffer paramsBuffer = nullptr; + vk::raii::DeviceMemory paramsBufferMemory = nullptr; + + // Command buffer for compute operations + vk::raii::CommandPool commandPool = nullptr; + vk::raii::CommandBuffer commandBuffer = nullptr; + }; + + VulkanResources vulkanResources; + + // Initialize Vulkan resources for physics simulation + bool InitializeVulkanResources(); + void CleanupVulkanResources(); + + // Update physics data on the GPU + void UpdateGPUPhysicsData(); + + // Read back physics data from the GPU + void ReadbackGPUPhysicsData(); + + // Perform GPU-accelerated physics simulation + void SimulatePhysicsOnGPU(float deltaTime); +}; diff --git a/attachments/simple_engine/pipeline.cpp b/attachments/simple_engine/pipeline.cpp new file mode 100644 index 00000000..0bf5b709 --- /dev/null +++ b/attachments/simple_engine/pipeline.cpp @@ -0,0 +1,632 @@ +#include "pipeline.h" +#include +#include + +// Constructor +Pipeline::Pipeline(VulkanDevice& device, SwapChain& swapChain) + : device(device), swapChain(swapChain) { +} + +// Destructor +Pipeline::~Pipeline() { + // RAII will handle destruction +} + +// Create descriptor set layout +bool Pipeline::createDescriptorSetLayout() { + try { + // Create descriptor set layout bindings + std::array bindings = { + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + } + }; + + // Create descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device.getDevice(), layoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +// Create graphics pipeline +bool Pipeline::createGraphicsPipeline() { + try { + // Read shader code + auto vertShaderCode = readFile("shaders/texturedMesh.spv"); + auto fragShaderCode = readFile("shaders/texturedMesh.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 0, + .pVertexBindingDescriptions = nullptr, + .vertexAttributeDescriptionCount = 0, + .pVertexAttributeDescriptions = nullptr + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChain.getSwapChainExtent().width), + .height = static_cast(swapChain.getSwapChainExtent().height), + .minDepth = 0.0f, + .maxDepth = 1.0f + }; + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChain.getSwapChainExtent() + }; + + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 1.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE, + .front = {}, + .back = {}, + .minDepthBounds = 0.0f, + .maxDepthBounds = 1.0f + }; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = vk::BlendFactor::eOne, + .dstColorBlendFactor = vk::BlendFactor::eZero, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eZero, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + .blendConstants = blendConstants + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr + }; + + pipelineLayout = vk::raii::PipelineLayout(device.getDevice(), pipelineLayoutInfo); + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + + // Create pipeline with dynamic rendering + vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + pipelineInfo.pNext = &renderingInfo; + + graphicsPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create PBR pipeline +bool Pipeline::createPBRPipeline() { + try { + // Read shader code + auto vertShaderCode = readFile("shaders/pbr.spv"); + auto fragShaderCode = readFile("shaders/pbr.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "FSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 0, + .pVertexBindingDescriptions = nullptr, + .vertexAttributeDescriptionCount = 0, + .pVertexAttributeDescriptions = nullptr + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChain.getSwapChainExtent().width), + .height = static_cast(swapChain.getSwapChainExtent().height), + .minDepth = 0.0f, + .maxDepth = 1.0f + }; + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChain.getSwapChainExtent() + }; + + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 1.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE, + .front = {}, + .back = {}, + .minDepthBounds = 0.0f, + .maxDepthBounds = 1.0f + }; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = vk::BlendFactor::eOne, + .dstColorBlendFactor = vk::BlendFactor::eZero, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eZero, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + .blendConstants = blendConstants + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create push constant range for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(MaterialProperties) + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + pbrPipelineLayout = vk::raii::PipelineLayout(device.getDevice(), pipelineLayoutInfo); + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + + // Create pipeline with dynamic rendering + vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + pipelineInfo.pNext = &renderingInfo; + + pbrGraphicsPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create lighting pipeline +bool Pipeline::createLightingPipeline() { + try { + // Read shader code + auto vertShaderCode = readFile("shaders/lighting.spv"); + auto fragShaderCode = readFile("shaders/lighting.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 0, + .pVertexBindingDescriptions = nullptr, + .vertexAttributeDescriptionCount = 0, + .pVertexAttributeDescriptions = nullptr + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChain.getSwapChainExtent().width), + .height = static_cast(swapChain.getSwapChainExtent().height), + .minDepth = 0.0f, + .maxDepth = 1.0f + }; + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChain.getSwapChainExtent() + }; + + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 1.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE, + .front = {}, + .back = {}, + .minDepthBounds = 0.0f, + .maxDepthBounds = 1.0f + }; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = vk::BlendFactor::eOne, + .dstColorBlendFactor = vk::BlendFactor::eZero, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eZero, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + .blendConstants = blendConstants + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr + }; + + lightingPipelineLayout = vk::raii::PipelineLayout(device.getDevice(), pipelineLayoutInfo); + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *lightingPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + + // Create pipeline with dynamic rendering + vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + pipelineInfo.pNext = &renderingInfo; + + lightingPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create lighting pipeline: " << e.what() << std::endl; + return false; + } +} + +// Push material properties +void Pipeline::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) { + commandBuffer.pushConstants(*pbrPipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, material); +} + +// Create shader module +vk::raii::ShaderModule Pipeline::createShaderModule(const std::vector& code) { + vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size(), + .pCode = reinterpret_cast(code.data()) + }; + + return vk::raii::ShaderModule(device.getDevice(), createInfo); +} + +// Read file +std::vector Pipeline::readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + size_t fileSize = file.tellg(); + std::vector buffer(fileSize); + + file.seekg(0); + file.read(buffer.data(), fileSize); + file.close(); + + return buffer; +} diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h new file mode 100644 index 00000000..599fd392 --- /dev/null +++ b/attachments/simple_engine/pipeline.h @@ -0,0 +1,162 @@ +#pragma once + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include +#include +#define GLM_FORCE_RADIANS +#include +#include + +#include "vulkan_device.h" +#include "swap_chain.h" + +/** + * @brief Structure for material properties. + */ +struct MaterialProperties { + alignas(16) glm::vec4 ambientColor; + alignas(16) glm::vec4 diffuseColor; + alignas(16) glm::vec4 specularColor; + alignas(4) float shininess; + alignas(4) float padding[3]; // Padding to ensure alignment +}; + +/** + * @brief Class for managing Vulkan pipelines. + */ +class Pipeline { +public: + /** + * @brief Constructor. + * @param device The Vulkan device. + * @param swapChain The swap chain. + */ + Pipeline(VulkanDevice& device, SwapChain& swapChain); + + /** + * @brief Destructor. + */ + ~Pipeline(); + + /** + * @brief Create the descriptor set layout. + * @return True if the descriptor set layout was created successfully, false otherwise. + */ + bool createDescriptorSetLayout(); + + /** + * @brief Create the graphics pipeline. + * @return True if the graphics pipeline was created successfully, false otherwise. + */ + bool createGraphicsPipeline(); + + /** + * @brief Create the PBR pipeline. + * @return True if the PBR pipeline was created successfully, false otherwise. + */ + bool createPBRPipeline(); + + /** + * @brief Create the lighting pipeline. + * @return True if the lighting pipeline was created successfully, false otherwise. + */ + bool createLightingPipeline(); + + /** + * @brief Push material properties to a command buffer. + * @param commandBuffer The command buffer. + * @param material The material properties. + */ + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material); + + /** + * @brief Get the descriptor set layout. + * @return The descriptor set layout. + */ + vk::raii::DescriptorSetLayout& getDescriptorSetLayout() { return descriptorSetLayout; } + + /** + * @brief Get the pipeline layout. + * @return The pipeline layout. + */ + vk::raii::PipelineLayout& getPipelineLayout() { return pipelineLayout; } + + /** + * @brief Get the graphics pipeline. + * @return The graphics pipeline. + */ + vk::raii::Pipeline& getGraphicsPipeline() { return graphicsPipeline; } + + /** + * @brief Get the PBR pipeline layout. + * @return The PBR pipeline layout. + */ + vk::raii::PipelineLayout& getPBRPipelineLayout() { return pbrPipelineLayout; } + + /** + * @brief Get the PBR graphics pipeline. + * @return The PBR graphics pipeline. + */ + vk::raii::Pipeline& getPBRGraphicsPipeline() { return pbrGraphicsPipeline; } + + /** + * @brief Get the lighting pipeline layout. + * @return The lighting pipeline layout. + */ + vk::raii::PipelineLayout& getLightingPipelineLayout() { return lightingPipelineLayout; } + + /** + * @brief Get the lighting pipeline. + * @return The lighting pipeline. + */ + vk::raii::Pipeline& getLightingPipeline() { return lightingPipeline; } + + /** + * @brief Get the compute pipeline layout. + * @return The compute pipeline layout. + */ + vk::raii::PipelineLayout& getComputePipelineLayout() { return computePipelineLayout; } + + /** + * @brief Get the compute pipeline. + * @return The compute pipeline. + */ + vk::raii::Pipeline& getComputePipeline() { return computePipeline; } + + /** + * @brief Get the compute descriptor set layout. + * @return The compute descriptor set layout. + */ + vk::raii::DescriptorSetLayout& getComputeDescriptorSetLayout() { return computeDescriptorSetLayout; } + +private: + // Vulkan device + VulkanDevice& device; + + // Swap chain + SwapChain& swapChain; + + // Pipelines + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + vk::raii::PipelineLayout pbrPipelineLayout = nullptr; + vk::raii::Pipeline pbrGraphicsPipeline = nullptr; + vk::raii::PipelineLayout lightingPipelineLayout = nullptr; + vk::raii::Pipeline lightingPipeline = nullptr; + + // Compute pipeline + vk::raii::PipelineLayout computePipelineLayout = nullptr; + vk::raii::Pipeline computePipeline = nullptr; + vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; + + // Descriptor set layout + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + + // Helper functions + vk::raii::ShaderModule createShaderModule(const std::vector& code); + std::vector readFile(const std::string& filename); +}; diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp new file mode 100644 index 00000000..37873eed --- /dev/null +++ b/attachments/simple_engine/platform.cpp @@ -0,0 +1,501 @@ +#include "platform.h" + +#include + +#if PLATFORM_ANDROID +// Android platform implementation + +AndroidPlatform::AndroidPlatform(android_app* androidApp) + : app(androidApp) { + // Set up the app's user data + app->userData = this; + + // Set up the command callback + app->onAppCmd = [](android_app* app, int32_t cmd) { + auto* platform = static_cast(app->userData); + + switch (cmd) { + case APP_CMD_INIT_WINDOW: + if (app->window != nullptr) { + // Get the window dimensions + ANativeWindow* window = app->window; + platform->width = ANativeWindow_getWidth(window); + platform->height = ANativeWindow_getHeight(window); + platform->windowResized = true; + + // Call the resize callback if set + if (platform->resizeCallback) { + platform->resizeCallback(platform->width, platform->height); + } + } + break; + + case APP_CMD_TERM_WINDOW: + // Window is being hidden or closed + break; + + case APP_CMD_WINDOW_RESIZED: + if (app->window != nullptr) { + // Get the new window dimensions + ANativeWindow* window = app->window; + platform->width = ANativeWindow_getWidth(window); + platform->height = ANativeWindow_getHeight(window); + platform->windowResized = true; + + // Call the resize callback if set + if (platform->resizeCallback) { + platform->resizeCallback(platform->width, platform->height); + } + } + break; + + default: + break; + } + }; +} + +bool AndroidPlatform::Initialize(const std::string& appName, int requestedWidth, int requestedHeight) { + // On Android, the window dimensions are determined by the device + if (app->window != nullptr) { + width = ANativeWindow_getWidth(app->window); + height = ANativeWindow_getHeight(app->window); + + // Get device information for performance optimizations + // This is important for mobile development to adapt to different device capabilities + DetectDeviceCapabilities(); + + // Set up power-saving mode based on battery level + SetupPowerSavingMode(); + + // Initialize touch input handling + InitializeTouchInput(); + + return true; + } + return false; +} + +void AndroidPlatform::Cleanup() { + // Nothing to clean up for Android +} + +bool AndroidPlatform::ProcessEvents() { + // Process Android events + int events; + android_poll_source* source; + + // Poll for events with a timeout of 0 (non-blocking) + while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0) { + if (source != nullptr) { + source->process(app, source); + } + + // Check if we are exiting + if (app->destroyRequested != 0) { + return false; + } + } + + return true; +} + +bool AndroidPlatform::HasWindowResized() { + bool resized = windowResized; + windowResized = false; + return resized; +} + +bool AndroidPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) { + if (app->window == nullptr) { + return false; + } + + VkAndroidSurfaceCreateInfoKHR createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; + createInfo.window = app->window; + + if (vkCreateAndroidSurfaceKHR(instance, &createInfo, nullptr, surface) != VK_SUCCESS) { + return false; + } + + return true; +} + +void AndroidPlatform::SetResizeCallback(std::function callback) { + resizeCallback = std::move(callback); +} + +void AndroidPlatform::SetMouseCallback(std::function callback) { + mouseCallback = std::move(callback); +} + +void AndroidPlatform::SetKeyboardCallback(std::function callback) { + keyboardCallback = std::move(callback); +} + +void AndroidPlatform::SetCharCallback(std::function callback) { + charCallback = std::move(callback); +} + +void AndroidPlatform::DetectDeviceCapabilities() { + if (!app) { + return; + } + + // Get API level + JNIEnv* env = nullptr; + app->activity->vm->AttachCurrentThread(&env, nullptr); + if (env) { + // Get Build.VERSION.SDK_INT + jclass versionClass = env->FindClass("android/os/Build$VERSION"); + jfieldID sdkFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I"); + deviceCapabilities.apiLevel = env->GetStaticIntField(versionClass, sdkFieldID); + + // Get device model and manufacturer + jclass buildClass = env->FindClass("android/os/Build"); + jfieldID modelFieldID = env->GetStaticFieldID(buildClass, "MODEL", "Ljava/lang/String;"); + jfieldID manufacturerFieldID = env->GetStaticFieldID(buildClass, "MANUFACTURER", "Ljava/lang/String;"); + + jstring modelJString = (jstring)env->GetStaticObjectField(buildClass, modelFieldID); + jstring manufacturerJString = (jstring)env->GetStaticObjectField(buildClass, manufacturerFieldID); + + const char* modelChars = env->GetStringUTFChars(modelJString, nullptr); + const char* manufacturerChars = env->GetStringUTFChars(manufacturerJString, nullptr); + + deviceCapabilities.deviceModel = modelChars; + deviceCapabilities.deviceManufacturer = manufacturerChars; + + env->ReleaseStringUTFChars(modelJString, modelChars); + env->ReleaseStringUTFChars(manufacturerJString, manufacturerChars); + + // Get CPU cores + jclass runtimeClass = env->FindClass("java/lang/Runtime"); + jmethodID getRuntime = env->GetStaticMethodID(runtimeClass, "getRuntime", "()Ljava/lang/Runtime;"); + jobject runtime = env->CallStaticObjectMethod(runtimeClass, getRuntime); + jmethodID availableProcessors = env->GetMethodID(runtimeClass, "availableProcessors", "()I"); + deviceCapabilities.cpuCores = env->CallIntMethod(runtime, availableProcessors); + + // Get total memory + jclass activityManagerClass = env->FindClass("android/app/ActivityManager"); + jclass memoryInfoClass = env->FindClass("android/app/ActivityManager$MemoryInfo"); + jmethodID memoryInfoConstructor = env->GetMethodID(memoryInfoClass, "", "()V"); + jobject memoryInfo = env->NewObject(memoryInfoClass, memoryInfoConstructor); + + jmethodID getSystemService = env->GetMethodID(env->GetObjectClass(app->activity->clazz), + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;"); + jstring serviceStr = env->NewStringUTF("activity"); + jobject activityManager = env->CallObjectMethod(app->activity->clazz, getSystemService, serviceStr); + + jmethodID getMemoryInfo = env->GetMethodID(activityManagerClass, "getMemoryInfo", + "(Landroid/app/ActivityManager$MemoryInfo;)V"); + env->CallVoidMethod(activityManager, getMemoryInfo, memoryInfo); + + jfieldID totalMemField = env->GetFieldID(memoryInfoClass, "totalMem", "J"); + deviceCapabilities.totalMemory = env->GetLongField(memoryInfo, totalMemField); + + env->DeleteLocalRef(serviceStr); + + // Check Vulkan support + // In a real implementation, this would check for Vulkan support and available extensions + deviceCapabilities.supportsVulkan = true; + deviceCapabilities.supportsVulkan11 = deviceCapabilities.apiLevel >= 28; // Android 9 (Pie) + deviceCapabilities.supportsVulkan12 = deviceCapabilities.apiLevel >= 29; // Android 10 + + // Add some common Vulkan extensions for mobile + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_MAINTENANCE1_EXTENSION_NAME); + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME); + + if (deviceCapabilities.apiLevel >= 28) { + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME); + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME); + } + + app->activity->vm->DetachCurrentThread(); + } + + LOGI("Device capabilities detected:"); + LOGI(" API Level: %d", deviceCapabilities.apiLevel); + LOGI(" Device: %s by %s", deviceCapabilities.deviceModel.c_str(), deviceCapabilities.deviceManufacturer.c_str()); + LOGI(" CPU Cores: %d", deviceCapabilities.cpuCores); + LOGI(" Total Memory: %lld bytes", (long long)deviceCapabilities.totalMemory); + LOGI(" Vulkan Support: %s", deviceCapabilities.supportsVulkan ? "Yes" : "No"); + LOGI(" Vulkan 1.1 Support: %s", deviceCapabilities.supportsVulkan11 ? "Yes" : "No"); + LOGI(" Vulkan 1.2 Support: %s", deviceCapabilities.supportsVulkan12 ? "Yes" : "No"); +} + +void AndroidPlatform::SetupPowerSavingMode() { + if (!app) { + return; + } + + // Check battery level and status + JNIEnv* env = nullptr; + app->activity->vm->AttachCurrentThread(&env, nullptr); + if (env) { + // Get battery level + jclass intentFilterClass = env->FindClass("android/content/IntentFilter"); + jmethodID intentFilterConstructor = env->GetMethodID(intentFilterClass, "", "(Ljava/lang/String;)V"); + jstring actionBatteryChanged = env->NewStringUTF("android.intent.action.BATTERY_CHANGED"); + jobject filter = env->NewObject(intentFilterClass, intentFilterConstructor, actionBatteryChanged); + + jmethodID registerReceiver = env->GetMethodID(env->GetObjectClass(app->activity->clazz), + "registerReceiver", + "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;"); + jobject intent = env->CallObjectMethod(app->activity->clazz, registerReceiver, nullptr, filter); + + if (intent) { + // Get battery level + jclass intentClass = env->GetObjectClass(intent); + jmethodID getIntExtra = env->GetMethodID(intentClass, "getIntExtra", "(Ljava/lang/String;I)I"); + + jstring levelKey = env->NewStringUTF("level"); + jstring scaleKey = env->NewStringUTF("scale"); + jstring statusKey = env->NewStringUTF("status"); + + int level = env->CallIntMethod(intent, getIntExtra, levelKey, -1); + int scale = env->CallIntMethod(intent, getIntExtra, scaleKey, -1); + int status = env->CallIntMethod(intent, getIntExtra, statusKey, -1); + + env->DeleteLocalRef(levelKey); + env->DeleteLocalRef(scaleKey); + env->DeleteLocalRef(statusKey); + + if (level != -1 && scale != -1) { + float batteryPct = (float)level / (float)scale; + + // Enable power-saving mode if battery is low (below 20%) and not charging + // Status values: 2 = charging, 3 = discharging, 4 = not charging, 5 = full + bool isCharging = (status == 2 || status == 5); + + if (batteryPct < 0.2f && !isCharging) { + EnablePowerSavingMode(true); + LOGI("Battery level low (%.0f%%), enabling power-saving mode", batteryPct * 100.0f); + } else { + LOGI("Battery level: %.0f%%, %s", batteryPct * 100.0f, isCharging ? "charging" : "not charging"); + } + } + } + + env->DeleteLocalRef(actionBatteryChanged); + app->activity->vm->DetachCurrentThread(); + } +} + +void AndroidPlatform::InitializeTouchInput() { + if (!app) { + return; + } + + // Set up input handling for touch events + app->onInputEvent = [](android_app* app, AInputEvent* event) -> int32_t { + auto* platform = static_cast(app->userData); + + if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { + int32_t action = AMotionEvent_getAction(event); + uint32_t flags = action & AMOTION_EVENT_ACTION_MASK; + + // Handle multi-touch if enabled + int32_t pointerCount = AMotionEvent_getPointerCount(event); + if (platform->IsMultiTouchEnabled() && pointerCount > 1) { + // In a real implementation, this would handle multi-touch gestures + // For now, just log the number of touch points + LOGI("Multi-touch event with %d pointers", pointerCount); + } + + // Convert touch event to mouse event for the engine + if (platform->mouseCallback) { + float x = AMotionEvent_getX(event, 0); + float y = AMotionEvent_getY(event, 0); + + uint32_t buttons = 0; + if (flags == AMOTION_EVENT_ACTION_DOWN || flags == AMOTION_EVENT_ACTION_MOVE) { + buttons |= 0x01; // Left button + } + + platform->mouseCallback(x, y, buttons); + } + + return 1; // Event handled + } + + return 0; // Event not handled + }; + + LOGI("Touch input initialized"); +} + +void AndroidPlatform::EnablePowerSavingMode(bool enable) { + powerSavingMode = enable; + + // In a real implementation, this would adjust rendering quality, update frequency, etc. + LOGI("Power-saving mode %s", enable ? "enabled" : "disabled"); + + // Example of what would be done in a real implementation: + // - Reduce rendering resolution + // - Lower frame rate + // - Disable post-processing effects + // - Reduce draw distance + // - Use simpler shaders +} + +#else +// Desktop platform implementation + +bool DesktopPlatform::Initialize(const std::string& appName, int requestedWidth, int requestedHeight) { + // Initialize GLFW + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW"); + } + + // GLFW was designed for OpenGL, so we need to tell it not to create an OpenGL context + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + // Create the window + window = glfwCreateWindow(requestedWidth, requestedHeight, appName.c_str(), nullptr, nullptr); + if (!window) { + glfwTerminate(); + throw std::runtime_error("Failed to create GLFW window"); + } + + // Set up the user pointer for callbacks + glfwSetWindowUserPointer(window, this); + + // Set up the callbacks + glfwSetFramebufferSizeCallback(window, WindowResizeCallback); + glfwSetCursorPosCallback(window, MousePositionCallback); + glfwSetMouseButtonCallback(window, MouseButtonCallback); + glfwSetKeyCallback(window, KeyCallback); + glfwSetCharCallback(window, CharCallback); + + // Get the initial window size + glfwGetFramebufferSize(window, &width, &height); + + return true; +} + +void DesktopPlatform::Cleanup() { + if (window) { + glfwDestroyWindow(window); + window = nullptr; + } + + glfwTerminate(); +} + +bool DesktopPlatform::ProcessEvents() { + // Process GLFW events + glfwPollEvents(); + + // Check if the window should close + return !glfwWindowShouldClose(window); +} + +bool DesktopPlatform::HasWindowResized() { + bool resized = windowResized; + windowResized = false; + return resized; +} + +bool DesktopPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) { + if (glfwCreateWindowSurface(instance, window, nullptr, surface) != VK_SUCCESS) { + return false; + } + + return true; +} + +void DesktopPlatform::SetResizeCallback(std::function callback) { + resizeCallback = std::move(callback); +} + +void DesktopPlatform::SetMouseCallback(std::function callback) { + mouseCallback = std::move(callback); +} + +void DesktopPlatform::SetKeyboardCallback(std::function callback) { + keyboardCallback = std::move(callback); +} + +void DesktopPlatform::SetCharCallback(std::function callback) { + charCallback = std::move(callback); +} + +void DesktopPlatform::WindowResizeCallback(GLFWwindow* window, int width, int height) { + auto* platform = static_cast(glfwGetWindowUserPointer(window)); + platform->width = width; + platform->height = height; + platform->windowResized = true; + + // Call the resize callback if set + if (platform->resizeCallback) { + platform->resizeCallback(width, height); + } +} + +void DesktopPlatform::MousePositionCallback(GLFWwindow* window, double xpos, double ypos) { + auto* platform = static_cast(glfwGetWindowUserPointer(window)); + + // Call the mouse callback if set + if (platform->mouseCallback) { + // Get the mouse button state + uint32_t buttons = 0; + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + buttons |= 0x01; // Left button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) { + buttons |= 0x02; // Right button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) { + buttons |= 0x04; // Middle button + } + + platform->mouseCallback(static_cast(xpos), static_cast(ypos), buttons); + } +} + +void DesktopPlatform::MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { + auto* platform = static_cast(glfwGetWindowUserPointer(window)); + + // Call the mouse callback if set + if (platform->mouseCallback) { + // Get the mouse position + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + + // Get the mouse button state + uint32_t buttons = 0; + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + buttons |= 0x01; // Left button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) { + buttons |= 0x02; // Right button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) { + buttons |= 0x04; // Middle button + } + + platform->mouseCallback(static_cast(xpos), static_cast(ypos), buttons); + } +} + +void DesktopPlatform::KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + auto* platform = static_cast(glfwGetWindowUserPointer(window)); + + // Call the keyboard callback if set + if (platform->keyboardCallback) { + platform->keyboardCallback(key, action != GLFW_RELEASE); + } +} + +void DesktopPlatform::CharCallback(GLFWwindow* window, unsigned int codepoint) { + auto* platform = static_cast(glfwGetWindowUserPointer(window)); + + // Call the char callback if set + if (platform->charCallback) { + platform->charCallback(codepoint); + } +} +#endif diff --git a/attachments/simple_engine/platform.h b/attachments/simple_engine/platform.h new file mode 100644 index 00000000..d3e6d746 --- /dev/null +++ b/attachments/simple_engine/platform.h @@ -0,0 +1,441 @@ +#pragma once + +#include +#include +#include + +#if PLATFORM_ANDROID +#include +#include +#include +#include +#include +#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "SimpleEngine", __VA_ARGS__)) +#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "SimpleEngine", __VA_ARGS__)) +#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "SimpleEngine", __VA_ARGS__)) +#else +#define GLFW_INCLUDE_VULKAN +#include +#define LOGI(...) printf(__VA_ARGS__); printf("\n") +#define LOGW(...) printf(__VA_ARGS__); printf("\n") +#define LOGE(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n") +#endif + +/** + * @brief Interface for platform-specific functionality. + * + * This class implements the platform abstraction as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + */ +class Platform { +public: + /** + * @brief Default constructor. + */ + Platform() = default; + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Platform() = default; + + /** + * @brief Initialize the platform. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + virtual bool Initialize(const std::string& appName, int width, int height) = 0; + + /** + * @brief Clean up platform resources. + */ + virtual void Cleanup() = 0; + + /** + * @brief Process platform events. + * @return True if the application should continue running, false if it should exit. + */ + virtual bool ProcessEvents() = 0; + + /** + * @brief Check if the window has been resized. + * @return True if the window has been resized, false otherwise. + */ + virtual bool HasWindowResized() = 0; + + /** + * @brief Get the current window width. + * @return The window width. + */ + virtual int GetWindowWidth() const = 0; + + /** + * @brief Get the current window height. + * @return The window height. + */ + virtual int GetWindowHeight() const = 0; + + /** + * @brief Get the current window size. + * @param width Pointer to store the window width. + * @param height Pointer to store the window height. + */ + virtual void GetWindowSize(int* width, int* height) const { + *width = GetWindowWidth(); + *height = GetWindowHeight(); + } + + /** + * @brief Create a Vulkan surface. + * @param instance The Vulkan instance. + * @param surface Pointer to the surface handle to be filled. + * @return True if the surface was created successfully, false otherwise. + */ + virtual bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) = 0; + + /** + * @brief Set a callback for window resize events. + * @param callback The callback function to be called when the window is resized. + */ + virtual void SetResizeCallback(std::function callback) = 0; + + /** + * @brief Set a callback for mouse input events. + * @param callback The callback function to be called when mouse input is received. + */ + virtual void SetMouseCallback(std::function callback) = 0; + + /** + * @brief Set a callback for keyboard input events. + * @param callback The callback function to be called when keyboard input is received. + */ + virtual void SetKeyboardCallback(std::function callback) = 0; + + /** + * @brief Set a callback for character input events. + * @param callback The callback function to be called when character input is received. + */ + virtual void SetCharCallback(std::function callback) = 0; +}; + +#if PLATFORM_ANDROID +/** + * @brief Android implementation of the Platform interface. + */ +class AndroidPlatform : public Platform { +private: + android_app* app = nullptr; + int width = 0; + int height = 0; + bool windowResized = false; + std::function resizeCallback; + std::function mouseCallback; + std::function keyboardCallback; + std::function charCallback; + + // Mobile-specific properties + struct DeviceCapabilities { + int apiLevel = 0; + std::string deviceModel; + std::string deviceManufacturer; + int cpuCores = 0; + int64_t totalMemory = 0; + bool supportsVulkan = false; + bool supportsVulkan11 = false; + bool supportsVulkan12 = false; + std::vector supportedVulkanExtensions; + }; + + DeviceCapabilities deviceCapabilities; + bool powerSavingMode = false; + bool multiTouchEnabled = true; + + /** + * @brief Detect device capabilities for performance optimizations. + */ + void DetectDeviceCapabilities(); + + /** + * @brief Set up power-saving mode based on battery level. + */ + void SetupPowerSavingMode(); + + /** + * @brief Initialize touch input handling. + */ + void InitializeTouchInput(); + +public: + /** + * @brief Enable or disable power-saving mode. + * @param enable Whether to enable power-saving mode. + */ + void EnablePowerSavingMode(bool enable); + + /** + * @brief Check if power-saving mode is enabled. + * @return True if power-saving mode is enabled, false otherwise. + */ + bool IsPowerSavingModeEnabled() const { return powerSavingMode; } + + /** + * @brief Enable or disable multi-touch input. + * @param enable Whether to enable multi-touch input. + */ + void EnableMultiTouch(bool enable) { multiTouchEnabled = enable; } + + /** + * @brief Check if multi-touch input is enabled. + * @return True if multi-touch input is enabled, false otherwise. + */ + bool IsMultiTouchEnabled() const { return multiTouchEnabled; } + + /** + * @brief Get the device capabilities. + * @return The device capabilities. + */ + const DeviceCapabilities& GetDeviceCapabilities() const { return deviceCapabilities; } + /** + * @brief Constructor with an Android app. + * @param androidApp The Android app. + */ + explicit AndroidPlatform(android_app* androidApp); + + /** + * @brief Initialize the platform. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, int width, int height) override; + + /** + * @brief Clean up platform resources. + */ + void Cleanup() override; + + /** + * @brief Process platform events. + * @return True if the application should continue running, false if it should exit. + */ + bool ProcessEvents() override; + + /** + * @brief Check if the window has been resized. + * @return True if the window has been resized, false otherwise. + */ + bool HasWindowResized() override; + + /** + * @brief Get the current window width. + * @return The window width. + */ + int GetWindowWidth() const override { return width; } + + /** + * @brief Get the current window height. + * @return The window height. + */ + int GetWindowHeight() const override { return height; } + + /** + * @brief Create a Vulkan surface. + * @param instance The Vulkan instance. + * @param surface Pointer to the surface handle to be filled. + * @return True if the surface was created successfully, false otherwise. + */ + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; + + /** + * @brief Set a callback for window resize events. + * @param callback The callback function to be called when the window is resized. + */ + void SetResizeCallback(std::function callback) override; + + /** + * @brief Set a callback for mouse input events. + * @param callback The callback function to be called when mouse input is received. + */ + void SetMouseCallback(std::function callback) override; + + /** + * @brief Set a callback for keyboard input events. + * @param callback The callback function to be called when keyboard input is received. + */ + void SetKeyboardCallback(std::function callback) override; + + /** + * @brief Set a callback for character input events. + * @param callback The callback function to be called when character input is received. + */ + void SetCharCallback(std::function callback) override; + + /** + * @brief Get the Android app. + * @return The Android app. + */ + android_app* GetApp() const { return app; } + + /** + * @brief Get the asset manager. + * @return The asset manager. + */ + AAssetManager* GetAssetManager() const { return app ? app->activity->assetManager : nullptr; } +}; +#else +/** + * @brief Desktop implementation of the Platform interface. + */ +class DesktopPlatform : public Platform { +private: + GLFWwindow* window = nullptr; + int width = 0; + int height = 0; + bool windowResized = false; + std::function resizeCallback; + std::function mouseCallback; + std::function keyboardCallback; + std::function charCallback; + + /** + * @brief Static callback for GLFW window resize events. + * @param window The GLFW window. + * @param width The new width. + * @param height The new height. + */ + static void WindowResizeCallback(GLFWwindow* window, int width, int height); + + /** + * @brief Static callback for GLFW mouse position events. + * @param window The GLFW window. + * @param xpos The x-coordinate of the cursor. + * @param ypos The y-coordinate of the cursor. + */ + static void MousePositionCallback(GLFWwindow* window, double xpos, double ypos); + + /** + * @brief Static callback for GLFW mouse button events. + * @param window The GLFW window. + * @param button The mouse button that was pressed or released. + * @param action The action (GLFW_PRESS or GLFW_RELEASE). + * @param mods The modifier keys that were held down. + */ + static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods); + + /** + * @brief Static callback for GLFW keyboard events. + * @param window The GLFW window. + * @param key The key that was pressed or released. + * @param scancode The system-specific scancode of the key. + * @param action The action (GLFW_PRESS, GLFW_RELEASE, or GLFW_REPEAT). + * @param mods The modifier keys that were held down. + */ + static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); + + /** + * @brief Static callback for GLFW character events. + * @param window The GLFW window. + * @param codepoint The Unicode code point of the character. + */ + static void CharCallback(GLFWwindow* window, unsigned int codepoint); + +public: + /** + * @brief Default constructor. + */ + DesktopPlatform() = default; + + /** + * @brief Initialize the platform. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, int width, int height) override; + + /** + * @brief Clean up platform resources. + */ + void Cleanup() override; + + /** + * @brief Process platform events. + * @return True if the application should continue running, false if it should exit. + */ + bool ProcessEvents() override; + + /** + * @brief Check if the window has been resized. + * @return True if the window has been resized, false otherwise. + */ + bool HasWindowResized() override; + + /** + * @brief Get the current window width. + * @return The window width. + */ + int GetWindowWidth() const override { return width; } + + /** + * @brief Get the current window height. + * @return The window height. + */ + int GetWindowHeight() const override { return height; } + + /** + * @brief Create a Vulkan surface. + * @param instance The Vulkan instance. + * @param surface Pointer to the surface handle to be filled. + * @return True if the surface was created successfully, false otherwise. + */ + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; + + /** + * @brief Set a callback for window resize events. + * @param callback The callback function to be called when the window is resized. + */ + void SetResizeCallback(std::function callback) override; + + /** + * @brief Set a callback for mouse input events. + * @param callback The callback function to be called when mouse input is received. + */ + void SetMouseCallback(std::function callback) override; + + /** + * @brief Set a callback for keyboard input events. + * @param callback The callback function to be called when keyboard input is received. + */ + void SetKeyboardCallback(std::function callback) override; + + /** + * @brief Set a callback for character input events. + * @param callback The callback function to be called when character input is received. + */ + void SetCharCallback(std::function callback) override; + + /** + * @brief Get the GLFW window. + * @return The GLFW window. + */ + GLFWwindow* GetWindow() const { return window; } +}; +#endif + +/** + * @brief Factory function for creating a platform instance. + * @param args Arguments to pass to the platform constructor. + * @return A unique pointer to the platform instance. + */ +template +std::unique_ptr CreatePlatform(Args&&... args) { +#if PLATFORM_ANDROID + return std::make_unique(std::forward(args)...); +#else + return std::make_unique(); +#endif +} diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h new file mode 100644 index 00000000..20c9ec82 --- /dev/null +++ b/attachments/simple_engine/renderer.h @@ -0,0 +1,398 @@ +#pragma once + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" +#include "entity.h" +#include "mesh_component.h" +#include "camera_component.h" + +/** + * @brief Structure for Vulkan queue family indices. + */ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + std::optional computeFamily; + + bool isComplete() const { + return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); + } +}; + +/** + * @brief Structure for swap chain support details. + */ +struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; + +/** + * @brief Structure for uniform buffer object. + */ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; + alignas(16) glm::vec4 lightPos; + alignas(16) glm::vec4 lightColor; + alignas(16) glm::vec4 viewPos; +}; + +/** + * @brief Structure for material properties. + */ +struct MaterialProperties { + alignas(16) glm::vec4 ambientColor; + alignas(16) glm::vec4 diffuseColor; + alignas(16) glm::vec4 specularColor; + alignas(4) float shininess; + alignas(4) float padding[3]; // Padding to ensure alignment +}; + +/** + * @brief Class for managing Vulkan rendering. + * + * This class implements the rendering pipeline as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc + */ +class Renderer { +public: + /** + * @brief Constructor with a platform. + * @param platform The platform to use for rendering. + */ + explicit Renderer(Platform* platform); + + /** + * @brief Destructor for proper cleanup. + */ + ~Renderer(); + + /** + * @brief Initialize the renderer. + * @param appName The name of the application. + * @param enableValidationLayers Whether to enable validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, bool enableValidationLayers = true); + + /** + * @brief Clean up renderer resources. + */ + void Cleanup(); + + /** + * @brief Render the scene. + * @param entities The entities to render. + * @param camera The camera to use for rendering. + */ + void Render(const std::vector& entities, CameraComponent* camera); + + /** + * @brief Wait for the device to be idle. + */ + void WaitIdle(); + + /** + * @brief Dispatch a compute shader. + * @param groupCountX The number of local workgroups to dispatch in the X dimension. + * @param groupCountY The number of local workgroups to dispatch in the Y dimension. + * @param groupCountZ The number of local workgroups to dispatch in the Z dimension. + * @param inputBuffer The input buffer. + * @param outputBuffer The output buffer. + * @param hrtfBuffer The HRTF data buffer. + * @param paramsBuffer The parameters buffer. + */ + void DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, + vk::Buffer inputBuffer, vk::Buffer outputBuffer, + vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer); + + /** + * @brief Check if the renderer is initialized. + * @return True if the renderer is initialized, false otherwise. + */ + bool IsInitialized() const { return initialized; } + + /** + * @brief Get the Vulkan device. + * @return The Vulkan device. + */ + vk::Device GetDevice() const { return *device; } + + /** + * @brief Get the Vulkan RAII device. + * @return The Vulkan RAII device. + */ + const vk::raii::Device& GetRaiiDevice() const { return device; } + + /** + * @brief Get the compute queue. + * @return The compute queue. + */ + vk::Queue GetComputeQueue() const { return *computeQueue; } + + /** + * @brief Find a suitable memory type. + * @param typeFilter The type filter. + * @param properties The memory properties. + * @return The memory type index. + */ + uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { + return findMemoryType(typeFilter, properties); + } + + /** + * @brief Create a shader module from SPIR-V code. + * @param code The SPIR-V code. + * @return The shader module. + */ + vk::raii::ShaderModule CreateShaderModule(const std::vector& code) { + return createShaderModule(code); + } + + /** + * @brief Create a shader module from a file. + * @param filename The filename. + * @return The shader module. + */ + vk::raii::ShaderModule CreateShaderModule(const std::string& filename) { + auto code = readFile(filename); + return createShaderModule(code); + } + + /** + * @brief Transition an image layout. + * @param image The image. + * @param format The image format. + * @param oldLayout The old layout. + * @param newLayout The new layout. + */ + void TransitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { + transitionImageLayout(image, format, oldLayout, newLayout); + } + + /** + * @brief Copy a buffer to an image. + * @param buffer The buffer. + * @param image The image. + * @param width The image width. + * @param height The image height. + */ + void CopyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { + copyBufferToImage(buffer, image, width, height); + } + + /** + * @brief Get the current command buffer. + * @return The current command buffer. + */ + vk::CommandBuffer GetCurrentCommandBuffer() const { + return *commandBuffers[currentFrame]; + } + +private: + // Platform + Platform* platform = nullptr; + + // Vulkan RAII context + vk::raii::Context context; + + // Vulkan instance and debug messenger + vk::raii::Instance instance = nullptr; + vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; + + // Vulkan device + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + + // Vulkan queues + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + vk::raii::Queue computeQueue = nullptr; + + // Vulkan surface + vk::raii::SurfaceKHR surface = nullptr; + + // Swap chain + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + vk::Extent2D swapChainExtent = {0, 0}; + std::vector swapChainImageViews; + + // Dynamic rendering info + vk::RenderingInfo renderingInfo; + std::vector colorAttachments; + vk::RenderingAttachmentInfo depthAttachment; + + // Pipelines + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + vk::raii::PipelineLayout pbrPipelineLayout = nullptr; + vk::raii::Pipeline pbrGraphicsPipeline = nullptr; + vk::raii::PipelineLayout lightingPipelineLayout = nullptr; + vk::raii::Pipeline lightingPipeline = nullptr; + + // Compute pipeline + vk::raii::PipelineLayout computePipelineLayout = nullptr; + vk::raii::Pipeline computePipeline = nullptr; + vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; + vk::raii::DescriptorPool computeDescriptorPool = nullptr; + std::vector computeDescriptorSets; + + // Command pool and buffers + vk::raii::CommandPool commandPool = nullptr; + std::vector commandBuffers; + + // Synchronization objects + std::vector imageAvailableSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + + // Depth buffer + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthImageMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + + // Descriptor set layout and pool + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::DescriptorPool descriptorPool = nullptr; + + // Mesh resources + struct MeshResources { + vk::raii::Buffer vertexBuffer = nullptr; + vk::raii::DeviceMemory vertexBufferMemory = nullptr; + vk::raii::Buffer indexBuffer = nullptr; + vk::raii::DeviceMemory indexBufferMemory = nullptr; + uint32_t indexCount = 0; + }; + std::unordered_map meshResources; + + // Texture resources + struct TextureResources { + vk::raii::Image textureImage = nullptr; + vk::raii::DeviceMemory textureImageMemory = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + }; + std::unordered_map textureResources; + + // Entity resources + struct EntityResources { + std::vector uniformBuffers; + std::vector uniformBuffersMemory; + std::vector uniformBuffersMapped; + std::vector descriptorSets; + }; + std::unordered_map entityResources; + + // Current frame index + uint32_t currentFrame = 0; + + // Queue family indices + QueueFamilyIndices queueFamilyIndices; + + // Validation layers + const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" + }; + + // Required device extensions + const std::vector requiredDeviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME + }; + + // Optional device extensions + const std::vector optionalDeviceExtensions = { + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, + VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME + }; + + // All device extensions (required + optional) + std::vector deviceExtensions; + + // Initialization flag + bool initialized = false; + + // Framebuffer resized flag + bool framebufferResized = false; + + // Maximum number of frames in flight + const uint32_t MAX_FRAMES_IN_FLIGHT = 2u; + + // Private methods + bool createInstance(const std::string& appName, bool enableValidationLayers); + bool setupDebugMessenger(bool enableValidationLayers); + bool createSurface(); + bool checkValidationLayerSupport(); + bool pickPhysicalDevice(); + void addSupportedOptionalExtensions(); + bool createLogicalDevice(bool enableValidationLayers); + bool createSwapChain(); + bool createImageViews(); + bool setupDynamicRendering(); + bool createDescriptorSetLayout(); + bool createGraphicsPipeline(); + bool createPBRPipeline(); + bool createLightingPipeline(); + bool createComputePipeline(); + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material); + bool createCommandPool(); + bool createDepthResources(); + bool createTextureImage(const std::string& texturePath, TextureResources& resources); + bool createTextureImageView(TextureResources& resources); + bool createTextureSampler(TextureResources& resources); + bool createMeshResources(MeshComponent* meshComponent); + bool createUniformBuffers(Entity* entity); + bool createDescriptorPool(); + bool createDescriptorSets(Entity* entity, const std::string& texturePath); + bool createCommandBuffers(); + bool createSyncObjects(); + + void cleanupSwapChain(); + void recreateSwapChain(); + + void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera); + + vk::raii::ShaderModule createShaderModule(const std::vector& code); + + QueueFamilyIndices findQueueFamilies(const vk::raii::PhysicalDevice& device); + SwapChainSupportDetails querySwapChainSupport(const vk::raii::PhysicalDevice& device); + bool isDeviceSuitable(vk::raii::PhysicalDevice& device); + bool checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device); + + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats); + vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes); + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities); + + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; + + std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); + void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size); + + std::pair createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties); + void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout); + void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height); + + vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags); + vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features); + vk::Format findDepthFormat(); + bool hasStencilComponent(vk::Format format); + + std::vector readFile(const std::string& filename); +}; diff --git a/attachments/simple_engine/renderer_compute.cpp b/attachments/simple_engine/renderer_compute.cpp new file mode 100644 index 00000000..56035864 --- /dev/null +++ b/attachments/simple_engine/renderer_compute.cpp @@ -0,0 +1,225 @@ +#include "renderer.h" +#include +#include +#include +#include + +// This file contains compute-related methods from the Renderer class + +// Create compute pipeline +bool Renderer::createComputePipeline() { + try { + // Read compute shader code + auto computeShaderCode = readFile("shaders/compute.comp.spv"); + + // Create shader module + vk::raii::ShaderModule computeShaderModule = createShaderModule(computeShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo computeShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eCompute, + .module = *computeShaderModule, + .pName = "main" + }; + + // Create compute descriptor set layout + std::array computeBindings = { + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr + }, + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr + }, + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr + }, + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr + } + }; + + vk::DescriptorSetLayoutCreateInfo computeLayoutInfo{ + .bindingCount = static_cast(computeBindings.size()), + .pBindings = computeBindings.data() + }; + + computeDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, computeLayoutInfo); + + // Create compute pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*computeDescriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr + }; + + computePipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create compute pipeline + vk::ComputePipelineCreateInfo pipelineInfo{ + .stage = computeShaderStageInfo, + .layout = *computePipelineLayout + }; + + computePipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + + // Create compute descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 4u * MAX_FRAMES_IN_FLIGHT + } + }; + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = MAX_FRAMES_IN_FLIGHT, + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data() + }; + + computeDescriptorPool = vk::raii::DescriptorPool(device, poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create compute pipeline: " << e.what() << std::endl; + return false; + } +} + +// Dispatch compute shader +void Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, + vk::Buffer inputBuffer, vk::Buffer outputBuffer, + vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer) { + try { + // Create descriptor sets + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *computeDescriptorPool, + .descriptorSetCount = 1, + .pSetLayouts = &*computeDescriptorSetLayout + }; + + computeDescriptorSets = device.allocateDescriptorSets(allocInfo); + + // Update descriptor sets + vk::DescriptorBufferInfo inputBufferInfo{ + .buffer = inputBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + vk::DescriptorBufferInfo outputBufferInfo{ + .buffer = outputBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + vk::DescriptorBufferInfo hrtfBufferInfo{ + .buffer = hrtfBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + vk::DescriptorBufferInfo paramsBufferInfo{ + .buffer = paramsBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + std::array descriptorWrites = { + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &inputBufferInfo + }, + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &outputBufferInfo + }, + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 2, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &hrtfBufferInfo + }, + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 3, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = ¶msBufferInfo + } + }; + + device.updateDescriptorSets(descriptorWrites, {}); + + // Create command buffer + vk::CommandBufferAllocateInfo cmdAllocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + auto commandBuffers = device.allocateCommandBuffers(cmdAllocInfo); + vk::CommandBuffer cmdBuffer = commandBuffers[0]; + vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Bind compute pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *computePipeline); + + // Bind descriptor sets + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, reinterpret_cast &>(computeDescriptorSets), {}); + + // Dispatch compute shader + commandBuffer.dispatch(groupCountX, groupCountY, groupCountZ); + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + computeQueue.submit(submitInfo, nullptr); + + // Wait for compute to complete + computeQueue.waitIdle(); + } catch (const std::exception& e) { + std::cerr << "Failed to dispatch compute shader: " << e.what() << std::endl; + } +} diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp new file mode 100644 index 00000000..d6e60f7a --- /dev/null +++ b/attachments/simple_engine/renderer_core.cpp @@ -0,0 +1,495 @@ +#include "renderer.h" +#include +#include +#include +#include +#include +#include + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include + +// Debug callback for vk::raii +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( + vk::DebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + vk::DebugUtilsMessageTypeFlagsEXT messageType, + const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { + // Print message to console + std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; + } + + return VK_FALSE; +} + +// Debug callback +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // Print message to console + std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; + } + + return VK_FALSE; +} + +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc + +// Constructor +Renderer::Renderer(Platform* platform) + : platform(platform) { + // Initialize deviceExtensions with required extensions only + // Optional extensions will be added later after checking device support + deviceExtensions = requiredDeviceExtensions; +} + +// Destructor +Renderer::~Renderer() { + Cleanup(); +} + +// Initialize the renderer +bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) { + // Create Vulkan instance + if (!createInstance(appName, enableValidationLayers)) { + return false; + } + + // Setup debug messenger + if (!setupDebugMessenger(enableValidationLayers)) { + return false; + } + + // Create surface + if (!createSurface()) { + return false; + } + + // Pick physical device + if (!pickPhysicalDevice()) { + return false; + } + + // Create logical device + if (!createLogicalDevice(enableValidationLayers)) { + return false; + } + + // Create swap chain + if (!createSwapChain()) { + return false; + } + + // Create image views + if (!createImageViews()) { + return false; + } + + // Setup dynamic rendering + if (!setupDynamicRendering()) { + return false; + } + + // Create descriptor set layout + if (!createDescriptorSetLayout()) { + return false; + } + + // Create graphics pipeline + if (!createGraphicsPipeline()) { + return false; + } + + // Create PBR pipeline + if (!createPBRPipeline()) { + return false; + } + + // Create lighting pipeline + if (!createLightingPipeline()) { + std::cerr << "Failed to create lighting pipeline" << std::endl; + return false; + } + + // // Create compute pipeline + // if (!createComputePipeline()) { + // std::cerr << "Failed to create compute pipeline" << std::endl; + // return false; + // } + + // Create command pool + if (!createCommandPool()) { + return false; + } + + // Create depth resources + if (!createDepthResources()) { + return false; + } + + // Create descriptor pool + if (!createDescriptorPool()) { + return false; + } + + // Create command buffers + if (!createCommandBuffers()) { + return false; + } + + // Create sync objects + if (!createSyncObjects()) { + return false; + } + + initialized = true; + return true; +} + +// Clean up renderer resources +void Renderer::Cleanup() { + if (initialized) { + // Wait for the device to be idle before cleaning up + device.waitIdle(); + + // Clean up swap chain + cleanupSwapChain(); + + // Clear resources - RAII will handle destruction + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + commandBuffers.clear(); + commandPool = nullptr; + descriptorPool = nullptr; + pbrGraphicsPipeline = nullptr; + pbrPipelineLayout = nullptr; + lightingPipeline = nullptr; + lightingPipelineLayout = nullptr; + graphicsPipeline = nullptr; + pipelineLayout = nullptr; + computePipeline = nullptr; + computePipelineLayout = nullptr; + computeDescriptorSetLayout = nullptr; + computeDescriptorPool = nullptr; + descriptorSetLayout = nullptr; + + // Clear mesh resources - RAII will handle destruction + meshResources.clear(); + + // Clear texture resources - RAII will handle destruction + textureResources.clear(); + + // Clear entity resources - RAII will handle destruction + entityResources.clear(); + + // Clear device, surface, debug messenger, and instance - RAII will handle destruction + device = nullptr; + surface = nullptr; + debugMessenger = nullptr; + instance = nullptr; + + initialized = false; + } +} + +// Create instance +bool Renderer::createInstance(const std::string& appName, bool enableValidationLayers) { + try { + // Create application info + vk::ApplicationInfo appInfo{ + .pApplicationName = appName.c_str(), + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "Simple Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_3 + }; + + // Get required extensions + std::vector extensions; + + // Add required extensions for GLFW +#if PLATFORM_DESKTOP + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); +#endif + + // Add debug extension if validation layers are enabled + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + // Create instance info + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data() + }; + + // Enable validation layers if requested + if (enableValidationLayers) { + if (!checkValidationLayerSupport()) { + std::cerr << "Validation layers requested, but not available" << std::endl; + return false; + } + + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + } + + // Create instance + instance = vk::raii::Instance(context, createInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create instance: " << e.what() << std::endl; + return false; + } +} + +// Setup debug messenger +bool Renderer::setupDebugMessenger(bool enableValidationLayers) { + if (!enableValidationLayers) { + return true; + } + + try { + // Create debug messenger info + vk::DebugUtilsMessengerCreateInfoEXT createInfo{ + .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError, + .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance, + .pfnUserCallback = debugCallbackVkRaii + }; + + // Create debug messenger + debugMessenger = vk::raii::DebugUtilsMessengerEXT(instance, createInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to set up debug messenger: " << e.what() << std::endl; + return false; + } +} + +// Create surface +bool Renderer::createSurface() { + try { + // Create surface + VkSurfaceKHR _surface; + if (!platform->CreateVulkanSurface(*instance, &_surface)) { + std::cerr << "Failed to create window surface" << std::endl; + return false; + } + + surface = vk::raii::SurfaceKHR(instance, _surface); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create surface: " << e.what() << std::endl; + return false; + } +} + +// Pick physical device +bool Renderer::pickPhysicalDevice() { + try { + // Get available physical devices + std::vector devices = instance.enumeratePhysicalDevices(); + + if (devices.empty()) { + std::cerr << "Failed to find GPUs with Vulkan support" << std::endl; + return false; + } + + // Find a suitable device using modern C++ ranges + const auto devIter = std::ranges::find_if( + devices, + [&](auto& device) { + // Print device properties for debugging + vk::PhysicalDeviceProperties deviceProperties = device.getProperties(); + std::cout << "Checking device: " << deviceProperties.deviceName << std::endl; + + // Check if device supports Vulkan 1.3 + bool supportsVulkan1_3 = deviceProperties.apiVersion >= VK_API_VERSION_1_3; + if (!supportsVulkan1_3) { + std::cout << " - Does not support Vulkan 1.3" << std::endl; + } + + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + bool supportsGraphics = indices.isComplete(); + if (!supportsGraphics) { + std::cout << " - Missing required queue families" << std::endl; + } + + // Check device extensions + bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(device); + if (!supportsAllRequiredExtensions) { + std::cout << " - Missing required extensions" << std::endl; + } + + // Check swap chain support + bool swapChainAdequate = false; + if (supportsAllRequiredExtensions) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + if (!swapChainAdequate) { + std::cout << " - Inadequate swap chain support" << std::endl; + } + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + if (!supportsRequiredFeatures) { + std::cout << " - Does not support required features (dynamicRendering)" << std::endl; + } + + return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && swapChainAdequate && supportsRequiredFeatures; + }); + + if (devIter != devices.end()) { + physicalDevice = *devIter; + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + std::cout << "Selected device: " << deviceProperties.deviceName << std::endl; + + // Store queue family indices for the selected device + queueFamilyIndices = findQueueFamilies(physicalDevice); + + // Add supported optional extensions + addSupportedOptionalExtensions(); + + return true; + } else { + std::cerr << "Failed to find a suitable GPU. Make sure your GPU supports Vulkan and has the required extensions." << std::endl; + return false; + } + } catch (const std::exception& e) { + std::cerr << "Failed to pick physical device: " << e.what() << std::endl; + return false; + } +} + +// Add supported optional extensions +void Renderer::addSupportedOptionalExtensions() { + try { + // Get available extensions + auto availableExtensions = physicalDevice.enumerateDeviceExtensionProperties(); + + // Check which optional extensions are supported and add them to deviceExtensions + for (const auto& optionalExt : optionalDeviceExtensions) { + bool supported = false; + for (const auto& availableExt : availableExtensions) { + if (strcmp(availableExt.extensionName, optionalExt) == 0) { + supported = true; + deviceExtensions.push_back(optionalExt); + std::cout << "Adding optional extension: " << optionalExt << std::endl; + break; + } + } + } + } catch (const std::exception& e) { + std::cerr << "Warning: Failed to add optional extensions: " << e.what() << std::endl; + } +} + +// Create logical device +bool Renderer::createLogicalDevice(bool enableValidationLayers) { + try { + // Create queue create infos for each unique queue family + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = { + queueFamilyIndices.graphicsFamily.value(), + queueFamilyIndices.presentFamily.value(), + queueFamilyIndices.computeFamily.value() + }; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + queueCreateInfos.push_back(queueCreateInfo); + } + + // Enable required features + auto features = physicalDevice.getFeatures2(); + features.features.samplerAnisotropy = vk::True; + + // Enable Vulkan 1.3 features + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + features.pNext = &vulkan13Features; + + // Create device + vk::DeviceCreateInfo createInfo{ + .pNext = &features, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + .pEnabledFeatures = nullptr // Using pNext for features + }; + + // Enable validation layers if requested + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + } + + // Create the logical device + device = vk::raii::Device(physicalDevice, createInfo); + + // Get queue handles + graphicsQueue = vk::raii::Queue(device, queueFamilyIndices.graphicsFamily.value(), 0); + presentQueue = vk::raii::Queue(device, queueFamilyIndices.presentFamily.value(), 0); + computeQueue = vk::raii::Queue(device, queueFamilyIndices.computeFamily.value(), 0); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create logical device: " << e.what() << std::endl; + return false; + } +} + +// Check validation layer support +bool Renderer::checkValidationLayerSupport() { + // Get available layers + std::vector availableLayers = context.enumerateInstanceLayerProperties(); + + // Check if all requested layers are available + for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } + } + + return true; +} diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp new file mode 100644 index 00000000..3e23fc09 --- /dev/null +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -0,0 +1,485 @@ +#include "renderer.h" +#include +#include +#include +#include +#include "mesh_component.h" + +// This file contains pipeline-related methods from the Renderer class + +// Create descriptor set layout +bool Renderer::createDescriptorSetLayout() { + try { + // Create binding for uniform buffer + vk::DescriptorSetLayoutBinding uboLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }; + + // Create binding for texture sampler + vk::DescriptorSetLayoutBinding samplerLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }; + + // Create descriptor set layout + std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +// Create graphics pipeline +bool Renderer::createGraphicsPipeline() { + try { + // Read shader code + auto vertShaderCode = readFile("shaders/texturedMesh.spv"); + auto fragShaderCode = readFile("shaders/texturedMesh.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + auto bindingDescription = Vertex::getBindingDescription(); + auto attributeDescriptions = Vertex::getAttributeDescriptions(); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), + .pVertexAttributeDescriptions = attributeDescriptions.data() + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr + }; + + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create pipeline rendering info + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = findDepthFormat() + }; + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pipelineLayout + }; + + graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create PBR pipeline +bool Renderer::createPBRPipeline() { + try { + // Read shader code + auto vertShaderCode = readFile("shaders/pbr.spv"); + auto fragShaderCode = readFile("shaders/pbr.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + auto bindingDescription = Vertex::getBindingDescription(); + auto attributeDescriptions = Vertex::getAttributeDescriptions(); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), + .pVertexAttributeDescriptions = attributeDescriptions.data() + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create push constant range for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(MaterialProperties) + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create pipeline rendering info + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = findDepthFormat() + }; + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout + }; + + pbrGraphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create lighting pipeline +bool Renderer::createLightingPipeline() { + try { + // Read shader code + auto vertShaderCode = readFile("shaders/lighting.spv"); + auto fragShaderCode = readFile("shaders/lighting.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + auto bindingDescription = Vertex::getBindingDescription(); + auto attributeDescriptions = Vertex::getAttributeDescriptions(); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), + .pVertexAttributeDescriptions = attributeDescriptions.data() + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_TRUE, + .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, + .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eZero, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr + }; + + lightingPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create pipeline rendering info + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = findDepthFormat() + }; + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *lightingPipelineLayout + }; + + lightingPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create lighting pipeline: " << e.what() << std::endl; + return false; + } +} + +// Push material properties to the pipeline +void Renderer::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) { + commandBuffer.pushConstants(*pbrPipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(MaterialProperties), &material); +} diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp new file mode 100644 index 00000000..39504656 --- /dev/null +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -0,0 +1,446 @@ +#include "renderer.h" +#include +#include +#include +#include +#include + +// This file contains rendering-related methods from the Renderer class + +// Create swap chain +bool Renderer::createSwapChain() { + try { + // Query swap chain support + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + // Choose swap surface format, present mode, and extent + vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities); + + // Choose image count + uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; + if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; + } + + // Create swap chain info + vk::SwapchainCreateInfoKHR createInfo{ + .surface = *surface, + .minImageCount = imageCount, + .imageFormat = surfaceFormat.format, + .imageColorSpace = surfaceFormat.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, + .preTransform = swapChainSupport.capabilities.currentTransform, + .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, + .presentMode = presentMode, + .clipped = VK_TRUE, + .oldSwapchain = nullptr + }; + + // Find queue families + QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + // Set sharing mode + if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = vk::SharingMode::eConcurrent; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; + } else { + createInfo.imageSharingMode = vk::SharingMode::eExclusive; + createInfo.queueFamilyIndexCount = 0; + createInfo.pQueueFamilyIndices = nullptr; + } + + // Create swap chain + swapChain = vk::raii::SwapchainKHR(device, createInfo); + + // Get swap chain images + swapChainImages = swapChain.getImages(); + + // Store swap chain format and extent + swapChainImageFormat = surfaceFormat.format; + swapChainExtent = extent; + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create swap chain: " << e.what() << std::endl; + return false; + } +} + +// Create image views +bool Renderer::createImageViews() { + try { + // Resize image views vector + swapChainImageViews.clear(); + swapChainImageViews.reserve(swapChainImages.size()); + + // Create image view for each swap chain image + for (const auto& image : swapChainImages) { + // Create image view info + vk::ImageViewCreateInfo createInfo{ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = swapChainImageFormat, + .components = { + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity + }, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + // Create image view + swapChainImageViews.emplace_back(device, createInfo); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create image views: " << e.what() << std::endl; + return false; + } +} + +// Setup dynamic rendering +bool Renderer::setupDynamicRendering() { + try { + // Create color attachment + colorAttachments = { + vk::RenderingAttachmentInfo{ + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f}) + } + }; + + // Create depth attachment + depthAttachment = vk::RenderingAttachmentInfo{ + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearDepthStencilValue(1.0f, 0) + }; + + // Create rendering info + renderingInfo = vk::RenderingInfo{ + .renderArea = vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent), + .layerCount = 1, + .colorAttachmentCount = static_cast(colorAttachments.size()), + .pColorAttachments = colorAttachments.data(), + .pDepthAttachment = &depthAttachment + }; + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to setup dynamic rendering: " << e.what() << std::endl; + return false; + } +} + +// Create command pool +bool Renderer::createCommandPool() { + try { + // Find queue families + QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); + + // Create command pool info + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + + // Create command pool + commandPool = vk::raii::CommandPool(device, poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create command pool: " << e.what() << std::endl; + return false; + } +} + +// Create command buffers +bool Renderer::createCommandBuffers() { + try { + // Resize command buffers vector + commandBuffers.clear(); + commandBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create command buffer allocation info + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = static_cast(MAX_FRAMES_IN_FLIGHT) + }; + + // Allocate command buffers + commandBuffers = vk::raii::CommandBuffers(device, allocInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create command buffers: " << e.what() << std::endl; + return false; + } +} + +// Create sync objects +bool Renderer::createSyncObjects() { + try { + // Resize semaphores and fences vectors + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + + imageAvailableSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + renderFinishedSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create semaphore and fence info + vk::SemaphoreCreateInfo semaphoreInfo{}; + vk::FenceCreateInfo fenceInfo{ + .flags = vk::FenceCreateFlagBits::eSignaled + }; + + // Create semaphores and fences for each frame in flight + for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + imageAvailableSemaphores.emplace_back(device, semaphoreInfo); + renderFinishedSemaphores.emplace_back(device, semaphoreInfo); + inFlightFences.emplace_back(device, fenceInfo); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create sync objects: " << e.what() << std::endl; + return false; + } +} + +// Clean up swap chain +void Renderer::cleanupSwapChain() { + // Clean up depth resources + depthImageView = nullptr; + depthImage = nullptr; + depthImageMemory = nullptr; + + // Clean up swap chain image views + swapChainImageViews.clear(); + + // Clean up swap chain + swapChain = nullptr; +} + +// Recreate swap chain +void Renderer::recreateSwapChain() { + // Wait for the device to be idle before recreating the swap chain + device.waitIdle(); + + // Clean up old swap chain resources + cleanupSwapChain(); + + // Recreate swap chain + createSwapChain(); + createImageViews(); + createDepthResources(); + createDescriptorPool(); + createGraphicsPipeline(); + createPBRPipeline(); + createLightingPipeline(); +} + +// Update uniform buffer +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera) { + // Get entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + return; + } + + // Get transform component + auto transformComponent = entity->GetComponent(); + if (!transformComponent) { + return; + } + + // Create uniform buffer object + UniformBufferObject ubo{}; + ubo.model = transformComponent->GetModelMatrix(); + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + ubo.proj[1][1] *= -1; // Flip Y for Vulkan + + // Set light position and color + ubo.lightPos = glm::vec4(5.0f, 5.0f, 5.0f, 1.0f); + ubo.lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); + ubo.viewPos = glm::vec4(camera->GetPosition(), 1.0f); + + // Copy to uniform buffer + std::memcpy(entityIt->second.uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +} + +// Render the scene +void Renderer::Render(const std::vector& entities, CameraComponent* camera) { + // Wait for the previous frame to finish + device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + + // Acquire the next image from the swap chain + uint32_t imageIndex; + auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); + imageIndex = result.second; + + // Check if the swap chain needs to be recreated + if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); + return; + } else if (result.first != vk::Result::eSuccess) { + throw std::runtime_error("Failed to acquire swap chain image"); + } + + // Reset the fence for the current frame + device.resetFences(*inFlightFences[currentFrame]); + + // Reset the command buffer + commandBuffers[currentFrame].reset(); + + // Record the command buffer + commandBuffers[currentFrame].begin(vk::CommandBufferBeginInfo()); + + // Update dynamic rendering attachments + colorAttachments[0].setImageView(*swapChainImageViews[imageIndex]); + depthAttachment.setImageView(*depthImageView); + + // Update rendering area + renderingInfo.setRenderArea(vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Begin dynamic rendering with vk::raii + commandBuffers[currentFrame].beginRendering(renderingInfo); + + // Bind the graphics pipeline + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); + + // Set the viewport + vk::Viewport viewport(0.0f, 0.0f, + static_cast(swapChainExtent.width), + static_cast(swapChainExtent.height), + 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport); + + // Set the scissor + vk::Rect2D scissor(vk::Offset2D(0, 0), swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor); + + // Render each entity + for (auto entity : entities) { + // Get the mesh component + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { + continue; + } + + // Get the transform component + auto transformComponent = entity->GetComponent(); + if (!transformComponent) { + continue; + } + + // Update the uniform buffer + updateUniformBuffer(currentFrame, entity, camera); + + // Get the mesh resources + auto meshIt = meshResources.find(meshComponent); + if (meshIt == meshResources.end()) { + // Create mesh resources if they don't exist + if (!createMeshResources(meshComponent)) { + continue; + } + meshIt = meshResources.find(meshComponent); + } + + // Get the entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + // Create entity resources if they don't exist + if (!createUniformBuffers(entity)) { + continue; + } + + // Create descriptor sets + if (!createDescriptorSets(entity, meshComponent->GetTexturePath())) { + continue; + } + + entityIt = entityResources.find(entity); + } + + // Bind the vertex buffer + std::array vertexBuffers = {*meshIt->second.vertexBuffer}; + std::array offsets = {0}; + commandBuffers[currentFrame].bindVertexBuffers(0, vertexBuffers, offsets); + + // Bind the index buffer + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + + // Bind the descriptor set + commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {entityIt->second.descriptorSets[currentFrame]}, {}); + + // Draw the mesh + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, 1, 0, 0, 0); + } + + // End dynamic rendering + commandBuffers[currentFrame].endRendering(); + + // End command buffer + commandBuffers[currentFrame].end(); + + // Submit command buffer + vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; + vk::SubmitInfo submitInfo{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*imageAvailableSemaphores[currentFrame], + .pWaitDstStageMask = waitStages, + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[currentFrame], + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*renderFinishedSemaphores[currentFrame] + }; + + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + + // Present the image + vk::PresentInfoKHR presentInfo{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*renderFinishedSemaphores[currentFrame], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex + }; + + try { + result.first = presentQueue.presentKHR(presentInfo); + } catch (const vk::OutOfDateKHRError&) { + framebufferResized = true; + } + + if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); + } else if (result.first != vk::Result::eSuccess) { + throw std::runtime_error("Failed to present swap chain image"); + } + + // Advance to the next frame + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp new file mode 100644 index 00000000..5f6f1dcd --- /dev/null +++ b/attachments/simple_engine/renderer_resources.cpp @@ -0,0 +1,725 @@ +#include "renderer.h" +#include +#include +#include +#include +#include + +// Define STB_IMAGE_IMPLEMENTATION before including stb_image.h to provide the implementation +#define STB_IMAGE_IMPLEMENTATION +#include + +// This file contains resource-related methods from the Renderer class + +// Create depth resources +bool Renderer::createDepthResources() { + try { + // Find depth format + vk::Format depthFormat = findDepthFormat(); + + // Create depth image + auto [depthImg, depthImgMem] = createImage( + swapChainExtent.width, + swapChainExtent.height, + depthFormat, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eDepthStencilAttachment, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + depthImage = std::move(depthImg); + depthImageMemory = std::move(depthImgMem); + + // Create depth image view + depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); + + // Transition depth image layout + transitionImageLayout( + *depthImage, + depthFormat, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eDepthStencilAttachmentOptimal + ); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create depth resources: " << e.what() << std::endl; + return false; + } +} + +// Create texture image +bool Renderer::createTextureImage(const std::string& texturePath, TextureResources& resources) { + try { + // Check if texture already exists + auto it = textureResources.find(texturePath); + if (it != textureResources.end()) { + resources = std::move(it->second); + return true; + } + + // Load texture image + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load(texturePath.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + if (!pixels) { + std::cerr << "Failed to load texture image: " << texturePath << std::endl; + return false; + } + + vk::DeviceSize imageSize = texWidth * texHeight * 4; + + // Create staging buffer + auto [stagingBuffer, stagingBufferMemory] = createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy pixel data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels, static_cast(imageSize)); + stagingBufferMemory.unmapMemory(); + + // Free pixel data + stbi_image_free(pixels); + + // Create texture image + auto [textureImg, textureImgMem] = createImage( + texWidth, + texHeight, + vk::Format::eR8G8B8A8Srgb, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + resources.textureImage = std::move(textureImg); + resources.textureImageMemory = std::move(textureImgMem); + + // Transition image layout for copy + transitionImageLayout( + *resources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal + ); + + // Copy buffer to image + copyBufferToImage( + *stagingBuffer, + *resources.textureImage, + static_cast(texWidth), + static_cast(texHeight) + ); + + // Transition image layout for shader access + transitionImageLayout( + *resources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eShaderReadOnlyOptimal + ); + + // Create texture image view + if (!createTextureImageView(resources)) { + return false; + } + + // Create texture sampler + if (!createTextureSampler(resources)) { + return false; + } + + // Add to texture resources map + textureResources[texturePath] = std::move(resources); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create texture image: " << e.what() << std::endl; + return false; + } +} + +// Create texture image view +bool Renderer::createTextureImageView(TextureResources& resources) { + try { + resources.textureImageView = createImageView( + resources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageAspectFlagBits::eColor + ); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create texture image view: " << e.what() << std::endl; + return false; + } +} + +// Create texture sampler +bool Renderer::createTextureSampler(TextureResources& resources) { + try { + // Get physical device properties + vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); + + // Create sampler + vk::SamplerCreateInfo samplerInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_TRUE, + .maxAnisotropy = properties.limits.maxSamplerAnisotropy, + .compareEnable = VK_FALSE, + .compareOp = vk::CompareOp::eAlways, + .minLod = 0.0f, + .maxLod = 0.0f, + .borderColor = vk::BorderColor::eIntOpaqueBlack, + .unnormalizedCoordinates = VK_FALSE + }; + + resources.textureSampler = vk::raii::Sampler(device, samplerInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create texture sampler: " << e.what() << std::endl; + return false; + } +} + +// Create mesh resources +bool Renderer::createMeshResources(MeshComponent* meshComponent) { + try { + // Check if mesh resources already exist + auto it = meshResources.find(meshComponent); + if (it != meshResources.end()) { + return true; + } + + // Get mesh data + const auto& vertices = meshComponent->GetVertices(); + const auto& indices = meshComponent->GetIndices(); + + if (vertices.empty() || indices.empty()) { + std::cerr << "Mesh has no vertices or indices" << std::endl; + return false; + } + + // Create vertex buffer + vk::DeviceSize vertexBufferSize = sizeof(vertices[0]) * vertices.size(); + auto [stagingVertexBuffer, stagingVertexBufferMemory] = createBuffer( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy vertex data to staging buffer + void* vertexData = stagingVertexBufferMemory.mapMemory(0, vertexBufferSize); + memcpy(vertexData, vertices.data(), static_cast(vertexBufferSize)); + stagingVertexBufferMemory.unmapMemory(); + + // Create vertex buffer on device + auto [vertexBuffer, vertexBufferMemory] = createBuffer( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Copy from staging buffer to device buffer + copyBuffer(stagingVertexBuffer, vertexBuffer, vertexBufferSize); + + // Create index buffer + vk::DeviceSize indexBufferSize = sizeof(indices[0]) * indices.size(); + auto [stagingIndexBuffer, stagingIndexBufferMemory] = createBuffer( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy index data to staging buffer + void* indexData = stagingIndexBufferMemory.mapMemory(0, indexBufferSize); + memcpy(indexData, indices.data(), static_cast(indexBufferSize)); + stagingIndexBufferMemory.unmapMemory(); + + // Create index buffer on device + auto [indexBuffer, indexBufferMemory] = createBuffer( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Copy from staging buffer to device buffer + copyBuffer(stagingIndexBuffer, indexBuffer, indexBufferSize); + + // Create mesh resources + MeshResources resources; + resources.vertexBuffer = std::move(vertexBuffer); + resources.vertexBufferMemory = std::move(vertexBufferMemory); + resources.indexBuffer = std::move(indexBuffer); + resources.indexBufferMemory = std::move(indexBufferMemory); + resources.indexCount = static_cast(indices.size()); + + // Add to mesh resources map + meshResources[meshComponent] = std::move(resources); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create mesh resources: " << e.what() << std::endl; + return false; + } +} + +// Create uniform buffers +bool Renderer::createUniformBuffers(Entity* entity) { + try { + // Check if entity resources already exist + auto it = entityResources.find(entity); + if (it != entityResources.end()) { + return true; + } + + // Create entity resources + EntityResources resources; + resources.uniformBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + resources.uniformBuffersMemory.reserve(MAX_FRAMES_IN_FLIGHT); + resources.uniformBuffersMapped.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create uniform buffers + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + auto [buffer, bufferMemory] = createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eUniformBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + resources.uniformBuffers[i] = std::move(buffer); + resources.uniformBuffersMemory[i] = std::move(bufferMemory); + resources.uniformBuffersMapped[i] = resources.uniformBuffersMemory[i].mapMemory(0, bufferSize); + } + + // Add to entity resources map + entityResources[entity] = std::move(resources); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create uniform buffers: " << e.what() << std::endl; + return false; + } +} + +// Create descriptor pool +bool Renderer::createDescriptorPool() { + try { + // Create descriptor pool sizes + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT * 100) + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT * 100) + } + }; + + // Create descriptor pool + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = static_cast(MAX_FRAMES_IN_FLIGHT * 100), + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data() + }; + + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } +} + +// Create descriptor sets +bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePath) { + try { + // Get entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + std::cerr << "Entity resources not found" << std::endl; + return false; + } + + // Get texture resources + TextureResources textureRes; + if (!texturePath.empty()) { + auto textureIt = textureResources.find(texturePath); + if (textureIt == textureResources.end()) { + // Create texture resources if they don't exist + if (!createTextureImage(texturePath, textureRes)) { + return false; + } + } else { + textureRes = std::move(textureIt->second); + } + } + + // Create descriptor sets + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; + + entityIt->second.descriptorSets = device.allocateDescriptorSets(allocInfo); + + // Update descriptor sets + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + // Uniform buffer descriptor + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *entityIt->second.uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + std::array descriptorWrites; + + // Uniform buffer descriptor write + descriptorWrites[0] = vk::WriteDescriptorSet{ + .dstSet = entityIt->second.descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }; + + // Texture sampler descriptor + vk::DescriptorImageInfo imageInfo{ + .sampler = *textureRes.textureSampler, + .imageView = *textureRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + // Texture sampler descriptor write + descriptorWrites[1] = vk::WriteDescriptorSet{ + .dstSet = entityIt->second.descriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + // Update descriptor sets + device.updateDescriptorSets(descriptorWrites, {}); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor sets: " << e.what() << std::endl; + return false; + } +} + +// Create buffer +std::pair Renderer::createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + try { + // Create buffer + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer buffer(device, bufferInfo); + + // Allocate memory + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory bufferMemory(device, allocInfo); + + // Bind memory to buffer + buffer.bindMemory(*bufferMemory, 0); + + return {std::move(buffer), std::move(bufferMemory)}; + } catch (const std::exception& e) { + std::cerr << "Failed to create buffer: " << e.what() << std::endl; + throw; + } +} + +// Copy buffer +void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size) { + try { + // Create command buffer + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + auto commandBuffers = device.allocateCommandBuffers(allocInfo); + vk::CommandBuffer cmdBuffer = commandBuffers[0]; + vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Copy buffer + vk::BufferCopy copyRegion{ + .srcOffset = 0, + .dstOffset = 0, + .size = size + }; + + commandBuffer.copyBuffer(*srcBuffer, *dstBuffer, copyRegion); + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } catch (const std::exception& e) { + std::cerr << "Failed to copy buffer: " << e.what() << std::endl; + throw; + } +} + +// Create image +std::pair Renderer::createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties) { + try { + // Create image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + vk::raii::Image image(device, imageInfo); + + // Allocate memory + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory imageMemory(device, allocInfo); + + // Bind memory to image + image.bindMemory(*imageMemory, 0); + + return {std::move(image), std::move(imageMemory)}; + } catch (const std::exception& e) { + std::cerr << "Failed to create image: " << e.what() << std::endl; + throw; + } +} + +// Create image view +vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags) { + try { + // Create image view + vk::ImageViewCreateInfo viewInfo{ + .image = *image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = aspectFlags, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + return vk::raii::ImageView(device, viewInfo); + } catch (const std::exception& e) { + std::cerr << "Failed to create image view: " << e.what() << std::endl; + throw; + } +} + +// Transition image layout +void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { + try { + // Create command buffer + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + auto commandBuffers = device.allocateCommandBuffers(allocInfo); + vk::CommandBuffer cmdBuffer = commandBuffers[0]; + vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Create image barrier + vk::ImageMemoryBarrier barrier{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint + ? vk::ImageAspectFlagBits::eDepth + : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + // Set access masks and pipeline stages based on layouts + vk::PipelineStageFlags sourceStage; + vk::PipelineStageFlags destinationStage; + + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; + + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eTransfer; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + sourceStage = vk::PipelineStageFlagBits::eTransfer; + destinationStage = vk::PipelineStageFlagBits::eFragmentShader; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; + + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; + } else { + throw std::invalid_argument("Unsupported layout transition!"); + } + + // Add barrier to command buffer + commandBuffer.pipelineBarrier( + sourceStage, destinationStage, + vk::DependencyFlagBits::eByRegion, + nullptr, + nullptr, + barrier + ); + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } catch (const std::exception& e) { + std::cerr << "Failed to transition image layout: " << e.what() << std::endl; + throw; + } +} + +// Copy buffer to image +void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { + try { + // Create command buffer + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + auto commandBuffers = device.allocateCommandBuffers(allocInfo); + vk::CommandBuffer cmdBuffer = commandBuffers[0]; + vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Create buffer image copy region + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + }; + + // Copy buffer to image + commandBuffer.copyBufferToImage( + buffer, + image, + vk::ImageLayout::eTransferDstOptimal, + region + ); + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } catch (const std::exception& e) { + std::cerr << "Failed to copy buffer to image: " << e.what() << std::endl; + throw; + } +} diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp new file mode 100644 index 00000000..c0f91b4d --- /dev/null +++ b/attachments/simple_engine/renderer_utils.cpp @@ -0,0 +1,269 @@ +#include "renderer.h" +#include +#include +#include +#include +#include + +// This file contains utility methods from the Renderer class + +// Find memory type +uint32_t Renderer::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { + try { + // Get memory properties + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + // Find suitable memory type + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); + } catch (const std::exception& e) { + std::cerr << "Failed to find memory type: " << e.what() << std::endl; + throw; + } +} + +// Find supported format +vk::Format Renderer::findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) { + try { + for (vk::Format format : candidates) { + vk::FormatProperties props = physicalDevice.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) { + return format; + } else if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("Failed to find supported format"); + } catch (const std::exception& e) { + std::cerr << "Failed to find supported format: " << e.what() << std::endl; + throw; + } +} + +// Find depth format +vk::Format Renderer::findDepthFormat() { + return findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment + ); +} + +// Check if format has stencil component +bool Renderer::hasStencilComponent(vk::Format format) { + return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; +} + +// Read file +std::vector Renderer::readFile(const std::string& filename) { + try { + // Open file at end to get size + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + // Get file size + size_t fileSize = file.tellg(); + std::vector buffer(fileSize); + + // Go back to beginning of file and read data + file.seekg(0); + file.read(buffer.data(), fileSize); + + // Close file + file.close(); + + return buffer; + } catch (const std::exception& e) { + std::cerr << "Failed to read file: " << e.what() << std::endl; + throw; + } +} + +// Create shader module +vk::raii::ShaderModule Renderer::createShaderModule(const std::vector& code) { + try { + // Create shader module + vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size(), + .pCode = reinterpret_cast(code.data()) + }; + + return vk::raii::ShaderModule(device, createInfo); + } catch (const std::exception& e) { + std::cerr << "Failed to create shader module: " << e.what() << std::endl; + throw; + } +} + +// Find queue families +QueueFamilyIndices Renderer::findQueueFamilies(const vk::raii::PhysicalDevice& device) { + QueueFamilyIndices indices; + + // Get queue family properties + std::vector queueFamilies = device.getQueueFamilyProperties(); + + // Find queue families that support graphics, compute, and present + for (uint32_t i = 0; i < queueFamilies.size(); i++) { + // Check for graphics support + if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) { + indices.graphicsFamily = i; + } + + // Check for compute support + if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eCompute) { + indices.computeFamily = i; + } + + // Check for present support + if (device.getSurfaceSupportKHR(i, surface)) { + indices.presentFamily = i; + } + + // If all queue families are found, break + if (indices.isComplete()) { + break; + } + } + + return indices; +} + +// Query swap chain support +SwapChainSupportDetails Renderer::querySwapChainSupport(const vk::raii::PhysicalDevice& device) { + SwapChainSupportDetails details; + + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(surface); + + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(surface); + + return details; +} + +// Check device extension support +bool Renderer::checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device) { + auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties(); + + // Print available extensions for debugging + std::cout << "Available extensions:" << std::endl; + for (const auto& extension : availableDeviceExtensions) { + std::cout << " " << extension.extensionName << std::endl; + } + + // Check if all required extensions are supported + std::set requiredExtensionsSet(requiredDeviceExtensions.begin(), requiredDeviceExtensions.end()); + + for (const auto& extension : availableDeviceExtensions) { + requiredExtensionsSet.erase(extension.extensionName); + } + + // Print missing required extensions + if (!requiredExtensionsSet.empty()) { + std::cout << "Missing required extensions:" << std::endl; + for (const auto& extension : requiredExtensionsSet) { + std::cout << " " << extension << std::endl; + } + return false; + } + + // Check which optional extensions are supported + std::set optionalExtensionsSet(optionalDeviceExtensions.begin(), optionalDeviceExtensions.end()); + std::cout << "Supported optional extensions:" << std::endl; + for (const auto& extension : availableDeviceExtensions) { + if (optionalExtensionsSet.find(extension.extensionName) != optionalExtensionsSet.end()) { + std::cout << " " << extension.extensionName << " (supported)" << std::endl; + } + } + + return true; +} + +// Check if device is suitable +bool Renderer::isDeviceSuitable(vk::raii::PhysicalDevice& device) { + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + + // Check device extensions + bool extensionsSupported = checkDeviceExtensionSupport(device); + + // Check swap chain support + bool swapChainAdequate = false; + if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportsRequiredFeatures; +} + +// Choose swap surface format +vk::SurfaceFormatKHR Renderer::chooseSwapSurfaceFormat(const std::vector& availableFormats) { + // Look for SRGB format + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { + return availableFormat; + } + } + + // If not found, return first available format + return availableFormats[0]; +} + +// Choose swap present mode +vk::PresentModeKHR Renderer::chooseSwapPresentMode(const std::vector& availablePresentModes) { + // Look for mailbox mode (triple buffering) + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) { + return availablePresentMode; + } + } + + // If not found, return FIFO mode (guaranteed to be available) + return vk::PresentModeKHR::eFifo; +} + +// Choose swap extent +vk::Extent2D Renderer::chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { + // Get framebuffer size + int width, height; + platform->GetWindowSize(&width, &height); + + // Create extent + vk::Extent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + // Clamp to min/max extent + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +} + +// Wait for device to be idle +void Renderer::WaitIdle() { + device.waitIdle(); +} diff --git a/attachments/simple_engine/resource_manager.cpp b/attachments/simple_engine/resource_manager.cpp new file mode 100644 index 00000000..74159a2b --- /dev/null +++ b/attachments/simple_engine/resource_manager.cpp @@ -0,0 +1,26 @@ +#include "resource_manager.h" + +// Most of the ResourceManager class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc + +bool Resource::Load() { + loaded = true; + return true; +} + +void Resource::Unload() { + loaded = false; +} + +void ResourceManager::UnloadAllResources() { + for (auto& typePair : resources) { + for (auto& resourcePair : typePair.second) { + resourcePair.second->Unload(); + } + typePair.second.clear(); + } + resources.clear(); +} diff --git a/attachments/simple_engine/resource_manager.h b/attachments/simple_engine/resource_manager.h new file mode 100644 index 00000000..3dadbee7 --- /dev/null +++ b/attachments/simple_engine/resource_manager.h @@ -0,0 +1,252 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @brief Base class for all resources. + */ +class Resource { +protected: + std::string resourceId; + bool loaded = false; + +public: + /** + * @brief Constructor with a resource ID. + * @param id The unique identifier for the resource. + */ + explicit Resource(const std::string& id) : resourceId(id) {} + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Resource() = default; + + /** + * @brief Get the resource ID. + * @return The resource ID. + */ + const std::string& GetId() const { return resourceId; } + + /** + * @brief Check if the resource is loaded. + * @return True if the resource is loaded, false otherwise. + */ + bool IsLoaded() const { return loaded; } + + /** + * @brief Load the resource. + * @return True if the resource was loaded successfully, false otherwise. + */ + virtual bool Load(); + + /** + * @brief Unload the resource. + */ + virtual void Unload(); +}; + +/** + * @brief Template class for resource handles. + * @tparam T The type of resource. + */ +template +class ResourceHandle { +private: + std::string resourceId; + class ResourceManager* resourceManager = nullptr; + +public: + /** + * @brief Default constructor. + */ + ResourceHandle() = default; + + /** + * @brief Constructor with a resource ID and resource manager. + * @param id The resource ID. + * @param manager The resource manager. + */ + ResourceHandle(const std::string& id, class ResourceManager* manager) + : resourceId(id), resourceManager(manager) {} + + /** + * @brief Get the resource. + * @return A pointer to the resource, or nullptr if not found. + */ + T* Get() const; + + /** + * @brief Check if the handle is valid. + * @return True if the handle is valid, false otherwise. + */ + bool IsValid() const; + + /** + * @brief Get the resource ID. + * @return The resource ID. + */ + const std::string& GetId() const { return resourceId; } + + /** + * @brief Convenience operator for accessing the resource. + * @return A pointer to the resource. + */ + T* operator->() const { return Get(); } + + /** + * @brief Convenience operator for dereferencing the resource. + * @return A reference to the resource. + */ + T& operator*() const { return *Get(); } + + /** + * @brief Convenience operator for checking if the handle is valid. + * @return True if the handle is valid, false otherwise. + */ + operator bool() const { return IsValid(); } +}; + +/** + * @brief Class for managing resources. + * + * This class implements the resource management system as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc + */ +class ResourceManager { +private: + std::unordered_map>> resources; + +public: + /** + * @brief Default constructor. + */ + ResourceManager() = default; + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~ResourceManager() = default; + + /** + * @brief Load a resource. + * @tparam T The type of resource. + * @tparam Args The types of arguments to pass to the resource constructor. + * @param id The resource ID. + * @param args The arguments to pass to the resource constructor. + * @return A handle to the resource. + */ + template + ResourceHandle LoadResource(const std::string& id, Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + // Check if the resource already exists + auto& typeResources = resources[std::type_index(typeid(T))]; + auto it = typeResources.find(id); + if (it != typeResources.end()) { + return ResourceHandle(id, this); + } + + // Create and load the resource + auto resource = std::make_unique(id, std::forward(args)...); + if (!resource->Load()) { + throw std::runtime_error("Failed to load resource: " + id); + } + + // Store the resource + typeResources[id] = std::move(resource); + return ResourceHandle(id, this); + } + + /** + * @brief Get a resource. + * @tparam T The type of resource. + * @param id The resource ID. + * @return A pointer to the resource, or nullptr if not found. + */ + template + T* GetResource(const std::string& id) { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + auto typeIt = resources.find(std::type_index(typeid(T))); + if (typeIt == resources.end()) { + return nullptr; + } + + auto& typeResources = typeIt->second; + auto resourceIt = typeResources.find(id); + if (resourceIt == typeResources.end()) { + return nullptr; + } + + return static_cast(resourceIt->second.get()); + } + + /** + * @brief Check if a resource exists. + * @tparam T The type of resource. + * @param id The resource ID. + * @return True if the resource exists, false otherwise. + */ + template + bool HasResource(const std::string& id) { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + auto typeIt = resources.find(std::type_index(typeid(T))); + if (typeIt == resources.end()) { + return false; + } + + auto& typeResources = typeIt->second; + return typeResources.find(id) != typeResources.end(); + } + + /** + * @brief Unload a resource. + * @tparam T The type of resource. + * @param id The resource ID. + * @return True if the resource was unloaded, false otherwise. + */ + template + bool UnloadResource(const std::string& id) { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + auto typeIt = resources.find(std::type_index(typeid(T))); + if (typeIt == resources.end()) { + return false; + } + + auto& typeResources = typeIt->second; + auto resourceIt = typeResources.find(id); + if (resourceIt == typeResources.end()) { + return false; + } + + resourceIt->second->Unload(); + typeResources.erase(resourceIt); + return true; + } + + /** + * @brief Unload all resources. + */ + void UnloadAllResources(); +}; + +// Implementation of ResourceHandle methods +template +T* ResourceHandle::Get() const { + if (!resourceManager) return nullptr; + return resourceManager->GetResource(resourceId); +} + +template +bool ResourceHandle::IsValid() const { + if (!resourceManager) return false; + return resourceManager->HasResource(resourceId); +} diff --git a/attachments/simple_engine/shaders/hrtf.slang b/attachments/simple_engine/shaders/hrtf.slang new file mode 100644 index 00000000..b84fd78b --- /dev/null +++ b/attachments/simple_engine/shaders/hrtf.slang @@ -0,0 +1,126 @@ +// Compute shader for HRTF (Head-Related Transfer Function) audio processing +// This shader processes audio data to create 3D spatial audio effects + +// Workgroup size +[[vk::compute_shader_input(local_size_x = 256)]] + +// Input/output buffer bindings +[[vk::binding(0, 0)]] RWStructuredBuffer inputAudioBuffer; // Raw audio samples +[[vk::binding(1, 0)]] RWStructuredBuffer outputAudioBuffer; // Processed audio samples +[[vk::binding(2, 0)]] StructuredBuffer hrtfData; // HRTF impulse responses +[[vk::binding(3, 0)]] ConstantBuffer params; // HRTF parameters + +// Parameters for HRTF processing +struct HRTFParams { + float3 listenerPosition; // Position of the listener + float3 listenerForward; // Forward direction of the listener + float3 listenerUp; // Up direction of the listener + float3 sourcePosition; // Position of the sound source + float sampleRate; // Audio sample rate + uint inputChannels; // Number of input channels (typically 1 or 2) + uint outputChannels; // Number of output channels (typically 2 for stereo) + uint hrtfSize; // Size of each HRTF impulse response + uint numHrtfPositions; // Number of HRTF positions in the dataset + float distanceAttenuation; // Distance attenuation factor + float dopplerFactor; // Doppler effect factor + float reverbMix; // Reverb mix factor +}; + +// Helper function to calculate the index of the closest HRTF in the dataset +uint FindClosestHRTF(float azimuth, float elevation) { + // This is a simplified implementation + // In a real implementation, this would find the closest HRTF in the dataset + // based on the azimuth and elevation angles + + // Normalize azimuth to [0, 360) degrees + azimuth = fmod(azimuth + 360.0, 360.0); + + // Clamp elevation to [-90, 90] degrees + elevation = clamp(elevation, -90.0, 90.0); + + // Calculate indices based on a typical HRTF dataset layout + // Assuming 10-degree resolution in azimuth and 15-degree in elevation + uint azimuthIndex = uint(round(azimuth / 10.0)) % 36; + uint elevationIndex = uint(round((elevation + 90.0) / 15.0)) % 13; + + // Calculate the final index + return elevationIndex * 36 + azimuthIndex; +} + +// Helper function to calculate azimuth and elevation angles +void CalculateAngles(float3 sourceDir, float3 listenerForward, float3 listenerUp, out float azimuth, out float elevation) { + // Calculate listener's right vector + float3 listenerRight = cross(listenerForward, listenerUp); + + // Create rotation matrix from listener's orientation + float3x3 rotation = float3x3( + listenerRight, + listenerUp, + listenerForward + ); + + // Transform source direction to listener's local space + float3 localDir = mul(rotation, sourceDir); + + // Calculate azimuth (horizontal angle) + azimuth = atan2(localDir.x, localDir.z) * 57.2957795; // Convert to degrees + + // Calculate elevation (vertical angle) + float horizontalLength = sqrt(localDir.x * localDir.x + localDir.z * localDir.z); + elevation = atan2(localDir.y, horizontalLength) * 57.2957795; // Convert to degrees +} + +// Main compute shader function +[shader("compute")] +void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if the thread is within bounds + if (index >= params.sampleRate) { + return; + } + + // Calculate direction from listener to source + float3 sourceDir = params.sourcePosition - params.listenerPosition; + float distance = length(sourceDir); + sourceDir = normalize(sourceDir); + + // Calculate azimuth and elevation angles + float azimuth, elevation; + CalculateAngles(sourceDir, params.listenerForward, params.listenerUp, azimuth, elevation); + + // Find the closest HRTF in the dataset + uint hrtfIndex = FindClosestHRTF(azimuth, elevation); + + // Apply distance attenuation + float attenuation = 1.0 / max(1.0, distance * params.distanceAttenuation); + + // Process audio for left and right ears + for (uint channel = 0; channel < params.outputChannels; channel++) { + float sum = 0.0; + + // Convolve input audio with HRTF impulse response + for (uint i = 0; i < params.hrtfSize; i++) { + if (index + i < params.sampleRate) { + // Get the HRTF sample for this channel and position + uint hrtfOffset = hrtfIndex * params.hrtfSize * params.outputChannels + channel * params.hrtfSize + i; + float hrtfSample = hrtfData[hrtfOffset]; + + // Get the input audio sample + float audioSample = 0.0; + if (index >= i) { + audioSample = inputAudioBuffer[index - i]; + } + + // Apply convolution + sum += audioSample * hrtfSample; + } + } + + // Apply distance attenuation + sum *= attenuation; + + // Write to output buffer + outputAudioBuffer[index * params.outputChannels + channel] = sum; + } +} diff --git a/attachments/simple_engine/shaders/imgui.slang b/attachments/simple_engine/shaders/imgui.slang new file mode 100644 index 00000000..d554df23 --- /dev/null +++ b/attachments/simple_engine/shaders/imgui.slang @@ -0,0 +1,51 @@ +// Combined vertex and fragment shader for ImGui rendering + +// Input from vertex buffer +struct VSInput { + float2 Position : POSITION; + float2 UV : TEXCOORD0; + float4 Color : COLOR0; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float2 UV : TEXCOORD0; + float4 Color : COLOR0; +}; + +// Push constants for transformation +struct PushConstants { + float2 Scale; + float2 Translate; +}; + +// Bindings +[[vk::push_constant]] PushConstants pushConstants; +[[vk::binding(0, 0)]] Texture2D fontTexture; +[[vk::binding(0, 0)]] SamplerState fontSampler; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position + output.Position = float4(input.Position * pushConstants.Scale + pushConstants.Translate, 0.0, 1.0); + + // Pass UV and color to fragment shader + output.UV = input.UV; + output.Color = input.Color; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample font texture and multiply by color + float4 color = input.Color * fontTexture.Sample(fontSampler, input.UV); + return color; +} diff --git a/attachments/simple_engine/shaders/lighting.slang b/attachments/simple_engine/shaders/lighting.slang new file mode 100644 index 00000000..1565e20f --- /dev/null +++ b/attachments/simple_engine/shaders/lighting.slang @@ -0,0 +1,95 @@ +// Combined vertex and fragment shader for basic lighting +// This shader implements the Phong lighting model with push constants for material properties + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos : POSITION; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; +}; + +// Uniform buffer for transformation matrices and light information +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPos; + float4 lightColor; + float4 viewPos; +}; + +// Push constants for material properties +struct PushConstants { + float4 ambientColor; + float4 diffuseColor; + float4 specularColor; + float shininess; +}; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Texture2D texSampler; +[[vk::binding(1, 0)]] SamplerState texSamplerSampler; + +// Push constants +[[vk::push_constant]] PushConstants material; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal to world space + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass texture coordinates + output.TexCoord = input.TexCoord; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample texture + float4 texColor = texSampler.Sample(texSamplerSampler, input.TexCoord); + + // Normalize vectors + float3 normal = normalize(input.Normal); + float3 lightDir = normalize(ubo.lightPos.xyz - input.WorldPos); + float3 viewDir = normalize(ubo.viewPos.xyz - input.WorldPos); + float3 reflectDir = reflect(-lightDir, normal); + + // Ambient + float3 ambient = material.ambientColor.rgb * ubo.lightColor.rgb; + + // Diffuse + float diff = max(dot(normal, lightDir), 0.0); + float3 diffuse = diff * material.diffuseColor.rgb * ubo.lightColor.rgb; + + // Specular + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + float3 specular = spec * material.specularColor.rgb * ubo.lightColor.rgb; + + // Combine components + float3 result = (ambient + diffuse + specular) * texColor.rgb; + + return float4(result, texColor.a); +} diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang new file mode 100644 index 00000000..06f50a80 --- /dev/null +++ b/attachments/simple_engine/shaders/pbr.slang @@ -0,0 +1,199 @@ +// Combined vertex and fragment shader for PBR rendering + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos : POSITION; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPositions[4]; + float4 lightColors[4]; + float4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +}; + +// Push constants for material properties +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +}; + +// Constants +static const float PI = 3.14159265359; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Texture2D baseColorMap; +[[vk::binding(1, 0)]] SamplerState baseColorSampler; +[[vk::binding(2, 0)]] Texture2D metallicRoughnessMap; +[[vk::binding(2, 0)]] SamplerState metallicRoughnessSampler; +[[vk::binding(3, 0)]] Texture2D normalMap; +[[vk::binding(3, 0)]] SamplerState normalSampler; +[[vk::binding(4, 0)]] Texture2D occlusionMap; +[[vk::binding(4, 0)]] SamplerState occlusionSampler; +[[vk::binding(5, 0)]] Texture2D emissiveMap; +[[vk::binding(5, 0)]] SamplerState emissiveSampler; + +[[vk::push_constant]] PushConstants material; + +// PBR functions +float DistributionGGX(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH2 = NdotH * NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + + return ggx1 * ggx2; +} + +float3 FresnelSchlick(float cosTheta, float3 F0) { + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal to world space + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass texture coordinates + output.UV = input.UV; + + // Pass tangent + output.Tangent = input.Tangent; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample material textures + float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float metallic = metallicRoughness.x * material.metallicFactor; + float roughness = metallicRoughness.y * material.roughnessFactor; + float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; + float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; + + // Calculate normal in tangent space + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + // Apply normal mapping + float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + float3 T = normalize(input.Tangent.xyz); + float3 B = normalize(cross(N, T)) * input.Tangent.w; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(tangentNormal, TBN)); + } + + // Calculate view and reflection vectors + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + float3 R = reflect(-V, N); + + // Calculate F0 (base reflectivity) + float3 F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + + // Initialize lighting + float3 Lo = float3(0.0, 0.0, 0.0); + + // Calculate lighting for each light + for (int i = 0; i < 4; i++) { + float3 lightPos = ubo.lightPositions[i].xyz; + float3 lightColor = ubo.lightColors[i].rgb; + + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + // Calculate half vector + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // Energy conservation + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Add to outgoing radiance + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + + // Add ambient and emissive + float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + float3 color = ambient + Lo + emissive; + + // HDR tonemapping and gamma correction + color = color / (color + float3(1.0, 1.0, 1.0)); + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + return float4(color, baseColor.a); +} diff --git a/attachments/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang new file mode 100644 index 00000000..3abf3034 --- /dev/null +++ b/attachments/simple_engine/shaders/physics.slang @@ -0,0 +1,319 @@ +// Compute shader for physics simulation +// This shader processes rigid body physics data to simulate physical interactions + +// Workgroup size +[[vk::compute_shader_input(local_size_x = 64)]] + +// Physics data structure +struct PhysicsData { + float4 position; // xyz = position, w = inverse mass + float4 rotation; // quaternion + float4 linearVelocity; // xyz = velocity, w = restitution + float4 angularVelocity; // xyz = angular velocity, w = friction + float4 force; // xyz = force, w = is kinematic (0 or 1) + float4 torque; // xyz = torque, w = use gravity (0 or 1) + float4 colliderData; // type-specific data (e.g., radius for spheres) + float4 colliderData2; // additional collider data (e.g., box half extents) +}; + +// Collision data structure +struct CollisionData { + uint bodyA; + uint bodyB; + float4 contactNormal; // xyz = normal, w = penetration depth + float4 contactPoint; // xyz = contact point, w = unused +}; + +// Input/output buffer bindings +[[vk::binding(0, 0)]] RWStructuredBuffer physicsBuffer; // Physics data +[[vk::binding(1, 0)]] RWStructuredBuffer collisionBuffer; // Collision data +[[vk::binding(2, 0)]] RWStructuredBuffer pairBuffer; // Potential collision pairs +[[vk::binding(3, 0)]] RWStructuredBuffer counterBuffer; // [0] = pair count, [1] = collision count + +// Parameters for physics simulation +[[vk::binding(4, 0)]] ConstantBuffer params; + +struct PhysicsParams { + float deltaTime; // Time step + float3 gravity; // Gravity vector + uint numBodies; // Number of rigid bodies + uint maxCollisions; // Maximum number of collisions +}; + +// Quaternion multiplication +float4 quatMul(float4 q1, float4 q2) { + return float4( + q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + ); +} + +// Quaternion normalization +float4 quatNormalize(float4 q) { + float len = length(q); + if (len > 0.0001) { + return q / len; + } + return float4(0, 0, 0, 1); +} + +// Integration shader - updates positions and velocities +[shader("compute")] +void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of bodies + if (index >= params.numBodies) { + return; + } + + // Get physics data for this body + PhysicsData body = physicsBuffer[index]; + + // Skip kinematic bodies + if (body.force.w > 0.5) { + return; + } + + // Apply gravity if enabled + if (body.torque.w > 0.5) { + body.force.xyz += params.gravity / body.position.w; + } + + // Integrate forces + body.linearVelocity.xyz += body.force.xyz * body.position.w * params.deltaTime; + body.angularVelocity.xyz += body.torque.xyz * params.deltaTime; // Simplified, should use inertia tensor + + // Apply damping + const float linearDamping = 0.01; + const float angularDamping = 0.01; + body.linearVelocity.xyz *= (1.0 - linearDamping); + body.angularVelocity.xyz *= (1.0 - angularDamping); + + // Integrate velocities + body.position.xyz += body.linearVelocity.xyz * params.deltaTime; + + // Update rotation + float4 angularVelocityQuat = float4(body.angularVelocity.xyz * 0.5, 0.0); + float4 rotationDelta = quatMul(angularVelocityQuat, body.rotation); + body.rotation = quatNormalize(body.rotation + rotationDelta * params.deltaTime); + + // Write updated data back to buffer + physicsBuffer[index] = body; +} + +// Compute AABB for a body +void computeAABB(PhysicsData body, out float3 min, out float3 max) { + // Default to a small AABB + min = body.position.xyz - float3(0.1, 0.1, 0.1); + max = body.position.xyz + float3(0.1, 0.1, 0.1); + + // Check collider type + int colliderType = int(body.colliderData.w); + + if (colliderType == 0) { // Sphere + float radius = body.colliderData.x; + float3 center = body.position.xyz + body.colliderData2.xyz; + min = center - float3(radius, radius, radius); + max = center + float3(radius, radius, radius); + } + else if (colliderType == 1) { // Box + float3 halfExtents = body.colliderData.xyz; + float3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } +} + +// Check if two AABBs overlap +bool aabbOverlap(float3 minA, float3 maxA, float3 minB, float3 maxB) { + return all(minA < maxB) && all(minB < maxA); +} + +// Broad phase collision detection - identifies potential collision pairs +[shader("compute")] +void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Calculate total number of pairs + uint numPairs = (params.numBodies * (params.numBodies - 1)) / 2; + + if (index >= numPairs) { + return; + } + + // Convert linear index to pair indices (i, j) where i < j + uint i = 0; + uint j = 0; + + // This is a mathematical formula to convert a linear index to a pair of indices + uint row = uint(floor(sqrt(float(2 * index + 0.25)) - 0.5)); + i = row; + j = index - (row * (row + 1)) / 2; + + // Ensure j > i + j += i + 1; + + // Get physics data for both bodies + PhysicsData bodyA = physicsBuffer[i]; + PhysicsData bodyB = physicsBuffer[j]; + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Skip if either body doesn't have a collider + if (bodyA.colliderData.w < 0 || bodyB.colliderData.w < 0) { + return; + } + + // Compute AABBs + float3 minA, maxA, minB, maxB; + computeAABB(bodyA, minA, maxA); + computeAABB(bodyB, minB, maxB); + + // Check for AABB overlap + if (aabbOverlap(minA, maxA, minB, maxB)) { + // Add to potential collision pairs + uint pairIndex; + InterlockedAdd(counterBuffer[0], 1, pairIndex); + + if (pairIndex < params.maxCollisions) { + pairBuffer[pairIndex] = uint2(i, j); + } + } +} + +// Narrow phase collision detection - detailed collision detection for potential pairs +[shader("compute")] +void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of potential pairs + uint numPairs = counterBuffer[0]; + if (index >= numPairs || index >= params.maxCollisions) { + return; + } + + // Get the pair of bodies + uint2 pair = pairBuffer[index]; + uint bodyIndexA = pair.x; + uint bodyIndexB = pair.y; + + PhysicsData bodyA = physicsBuffer[bodyIndexA]; + PhysicsData bodyB = physicsBuffer[bodyIndexB]; + + // Determine collision shapes + int shapeA = int(bodyA.colliderData.w); + int shapeB = int(bodyB.colliderData.w); + + // Only handle sphere-sphere collisions for simplicity + if (shapeA == 0 && shapeB == 0) { // Both are spheres + float radiusA = bodyA.colliderData.x; + float radiusB = bodyB.colliderData.x; + + float3 posA = bodyA.position.xyz + bodyA.colliderData2.xyz; + float3 posB = bodyB.position.xyz + bodyB.colliderData2.xyz; + + float3 direction = posB - posA; + float distance = length(direction); + float minDistance = radiusA + radiusB; + + if (distance < minDistance) { + // Collision detected + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + + if (collisionIndex < params.maxCollisions) { + // Normalize direction + float3 normal = direction / max(distance, 0.0001); + + // Create collision data + CollisionData collision; + collision.bodyA = bodyIndexA; + collision.bodyB = bodyIndexB; + collision.contactNormal = float4(normal, minDistance - distance); // penetration depth + collision.contactPoint = float4(posA + normal * radiusA, 0); + + // Store collision data + collisionBuffer[collisionIndex] = collision; + } + } + } + // Add other collision types here (box-box, sphere-box, etc.) +} + +// Collision resolution - resolves detected collisions +[shader("compute")] +void ResolveCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of collisions + uint numCollisions = counterBuffer[1]; + if (index >= numCollisions || index >= params.maxCollisions) { + return; + } + + // Get collision data + CollisionData collision = collisionBuffer[index]; + + // Get the bodies involved in the collision + PhysicsData bodyA = physicsBuffer[collision.bodyA]; + PhysicsData bodyB = physicsBuffer[collision.bodyB]; + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Calculate relative velocity + float3 relativeVelocity = bodyB.linearVelocity.xyz - bodyA.linearVelocity.xyz; + + // Calculate velocity along normal + float velocityAlongNormal = dot(relativeVelocity, collision.contactNormal.xyz); + + // Don't resolve if velocities are separating + if (velocityAlongNormal > 0) { + return; + } + + // Calculate restitution (bounciness) + float restitution = min(bodyA.linearVelocity.w, bodyB.linearVelocity.w); + + // Calculate impulse scalar + float j = -(1.0 + restitution) * velocityAlongNormal; + j /= bodyA.position.w + bodyB.position.w; + + // Apply impulse + float3 impulse = collision.contactNormal.xyz * j; + + // Update velocities + if (bodyA.force.w < 0.5) { // If not kinematic + bodyA.linearVelocity.xyz -= impulse * bodyA.position.w; + physicsBuffer[collision.bodyA] = bodyA; + } + + if (bodyB.force.w < 0.5) { // If not kinematic + bodyB.linearVelocity.xyz += impulse * bodyB.position.w; + physicsBuffer[collision.bodyB] = bodyB; + } + + // Position correction to prevent sinking + const float percent = 0.2; // usually 20% to 80% + const float slop = 0.01; // small penetration allowed + float3 correction = max(collision.contactNormal.w - slop, 0.0) * percent * collision.contactNormal.xyz / (bodyA.position.w + bodyB.position.w); + + if (bodyA.force.w < 0.5) { // If not kinematic + bodyA.position.xyz -= correction * bodyA.position.w; + physicsBuffer[collision.bodyA] = bodyA; + } + + if (bodyB.force.w < 0.5) { // If not kinematic + bodyB.position.xyz += correction * bodyB.position.w; + physicsBuffer[collision.bodyB] = bodyB; + } +} diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang new file mode 100644 index 00000000..3e86d615 --- /dev/null +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -0,0 +1,52 @@ +// Combined vertex and fragment shader for textured mesh rendering +// This shader provides basic textured rendering with a uniform color + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Color : COLOR; + float2 TexCoord : TEXCOORD0; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 Color : COLOR; + float2 TexCoord : TEXCOORD0; +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; +}; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Texture2D texSampler; +[[vk::binding(1, 0)]] SamplerState texSamplerSampler; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + output.Position = mul(ubo.proj, mul(ubo.view, mul(ubo.model, float4(input.Position, 1.0)))); + + // Pass color and texture coordinates to fragment shader + output.Color = input.Color; + output.TexCoord = input.TexCoord; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample texture and multiply by color + return texSampler.Sample(texSamplerSampler, input.TexCoord) * float4(input.Color, 1.0); +} diff --git a/attachments/simple_engine/swap_chain.h b/attachments/simple_engine/swap_chain.h new file mode 100644 index 00000000..1b79067c --- /dev/null +++ b/attachments/simple_engine/swap_chain.h @@ -0,0 +1,103 @@ +#pragma once + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include +#include +#include + +#include "vulkan_device.h" +#include "platform.h" + +/** + * @brief Class for managing the Vulkan swap chain. + */ +class SwapChain { +public: + /** + * @brief Constructor. + * @param device The Vulkan device. + * @param platform The platform. + */ + SwapChain(VulkanDevice& device, Platform* platform); + + /** + * @brief Destructor. + */ + ~SwapChain(); + + /** + * @brief Create the swap chain. + * @return True if the swap chain was created successfully, false otherwise. + */ + bool create(); + + /** + * @brief Create image views for the swap chain images. + * @return True if the image views were created successfully, false otherwise. + */ + bool createImageViews(); + + /** + * @brief Clean up the swap chain. + */ + void cleanup(); + + /** + * @brief Recreate the swap chain. + * @return True if the swap chain was recreated successfully, false otherwise. + */ + bool recreate(); + + /** + * @brief Get the swap chain. + * @return The swap chain. + */ + vk::raii::SwapchainKHR& getSwapChain() { return swapChain; } + + /** + * @brief Get the swap chain images. + * @return The swap chain images. + */ + const std::vector& getSwapChainImages() const { return swapChainImages; } + + /** + * @brief Get the swap chain image format. + * @return The swap chain image format. + */ + vk::Format getSwapChainImageFormat() const { return swapChainImageFormat; } + + /** + * @brief Get the swap chain extent. + * @return The swap chain extent. + */ + vk::Extent2D getSwapChainExtent() const { return swapChainExtent; } + + /** + * @brief Get the swap chain image views. + * @return The swap chain image views. + */ + const std::vector& getSwapChainImageViews() const { return swapChainImageViews; } + +private: + // Vulkan device + VulkanDevice& device; + + // Platform + Platform* platform; + + // Swap chain + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + vk::Extent2D swapChainExtent = {0, 0}; + std::vector swapChainImageViews; + + // Helper functions + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats); + vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes); + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities); +}; diff --git a/attachments/simple_engine/transform_component.cpp b/attachments/simple_engine/transform_component.cpp new file mode 100644 index 00000000..0682d47d --- /dev/null +++ b/attachments/simple_engine/transform_component.cpp @@ -0,0 +1,28 @@ +#include "transform_component.h" + +// Most of the TransformComponent class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// This implementation corresponds to the Camera_Transformations chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix + +// Returns the model matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix +const glm::mat4& TransformComponent::GetModelMatrix() { + if (matrixDirty) { + UpdateModelMatrix(); + } + return modelMatrix; +} + +// Updates the model matrix based on position, rotation, and scale +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix +void TransformComponent::UpdateModelMatrix() { + modelMatrix = glm::mat4(1.0f); + modelMatrix = glm::translate(modelMatrix, position); + modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); + modelMatrix = glm::scale(modelMatrix, scale); + matrixDirty = false; +} diff --git a/attachments/simple_engine/transform_component.h b/attachments/simple_engine/transform_component.h new file mode 100644 index 00000000..1e70a034 --- /dev/null +++ b/attachments/simple_engine/transform_component.h @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include + +#include "component.h" + +/** + * @brief Component that handles the position, rotation, and scale of an entity. + * + * This class implements the transform system as described in the Camera_Transformations chapter: + * @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix + */ +class TransformComponent : public Component { +private: + glm::vec3 position = {0.0f, 0.0f, 0.0f}; + glm::vec3 rotation = {0.0f, 0.0f, 0.0f}; // Euler angles in radians + glm::vec3 scale = {1.0f, 1.0f, 1.0f}; + + glm::mat4 modelMatrix = glm::mat4(1.0f); + bool matrixDirty = true; + +public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit TransformComponent(const std::string& componentName = "TransformComponent") + : Component(componentName) {} + + /** + * @brief Set the position of the entity. + * @param newPosition The new position. + */ + void SetPosition(const glm::vec3& newPosition) { + position = newPosition; + matrixDirty = true; + } + + /** + * @brief Get the position of the entity. + * @return The position. + */ + const glm::vec3& GetPosition() const { + return position; + } + + /** + * @brief Set the rotation of the entity using Euler angles. + * @param newRotation The new rotation in radians. + */ + void SetRotation(const glm::vec3& newRotation) { + rotation = newRotation; + matrixDirty = true; + } + + /** + * @brief Get the rotation of the entity as Euler angles. + * @return The rotation in radians. + */ + const glm::vec3& GetRotation() const { + return rotation; + } + + /** + * @brief Set the scale of the entity. + * @param newScale The new scale. + */ + void SetScale(const glm::vec3& newScale) { + scale = newScale; + matrixDirty = true; + } + + /** + * @brief Get the scale of the entity. + * @return The scale. + */ + const glm::vec3& GetScale() const { + return scale; + } + + /** + * @brief Set the uniform scale of the entity. + * @param uniformScale The new uniform scale. + */ + void SetUniformScale(float uniformScale) { + scale = glm::vec3(uniformScale); + matrixDirty = true; + } + + /** + * @brief Translate the entity relative to its current position. + * @param translation The translation to apply. + */ + void Translate(const glm::vec3& translation) { + position += translation; + matrixDirty = true; + } + + /** + * @brief Rotate the entity relative to its current rotation. + * @param eulerAngles The rotation to apply in radians. + */ + void Rotate(const glm::vec3& eulerAngles) { + rotation += eulerAngles; + matrixDirty = true; + } + + /** + * @brief Scale the entity relative to its current scale. + * @param scaleFactors The scale factors to apply. + */ + void Scale(const glm::vec3& scaleFactors) { + scale *= scaleFactors; + matrixDirty = true; + } + + /** + * @brief Get the model matrix for this transform. + * @return The model matrix. + */ + const glm::mat4& GetModelMatrix(); + +private: + /** + * @brief Update the model matrix based on position, rotation, and scale. + */ + void UpdateModelMatrix(); +}; diff --git a/attachments/simple_engine/vulkan_device.cpp b/attachments/simple_engine/vulkan_device.cpp new file mode 100644 index 00000000..2423c299 --- /dev/null +++ b/attachments/simple_engine/vulkan_device.cpp @@ -0,0 +1,288 @@ +#include "vulkan_device.h" +#include +#include +#include +#include +#include + +// Constructor +VulkanDevice::VulkanDevice(vk::raii::Instance& instance, vk::raii::SurfaceKHR& surface, + const std::vector& requiredExtensions, + const std::vector& optionalExtensions) + : instance(instance), surface(surface), + requiredExtensions(requiredExtensions), + optionalExtensions(optionalExtensions) { + + // Initialize deviceExtensions with required extensions + deviceExtensions = requiredExtensions; + + // Add optional extensions + deviceExtensions.insert(deviceExtensions.end(), optionalExtensions.begin(), optionalExtensions.end()); +} + +// Destructor +VulkanDevice::~VulkanDevice() { + // RAII will handle destruction +} + +// Pick physical device - improved implementation based on 37_multithreading.cpp +bool VulkanDevice::pickPhysicalDevice() { + try { + // Get available physical devices + std::vector devices = instance.enumeratePhysicalDevices(); + + if (devices.empty()) { + std::cerr << "Failed to find GPUs with Vulkan support" << std::endl; + return false; + } + + // Find a suitable device using modern C++ ranges + const auto devIter = std::ranges::find_if( + devices, + [&](auto& device) { + // Print device properties for debugging + vk::PhysicalDeviceProperties deviceProperties = device.getProperties(); + std::cout << "Checking device: " << deviceProperties.deviceName << std::endl; + + // Check if device supports Vulkan 1.3 + bool supportsVulkan1_3 = deviceProperties.apiVersion >= vk::ApiVersion13; + if (!supportsVulkan1_3) { + std::cout << " - Does not support Vulkan 1.3" << std::endl; + } + + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + bool supportsGraphics = indices.isComplete(); + if (!supportsGraphics) { + std::cout << " - Missing required queue families" << std::endl; + } + + // Check device extensions + bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(device); + if (!supportsAllRequiredExtensions) { + std::cout << " - Missing required extensions" << std::endl; + } + + // Check swap chain support + bool swapChainAdequate = false; + if (supportsAllRequiredExtensions) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + if (!swapChainAdequate) { + std::cout << " - Inadequate swap chain support" << std::endl; + } + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + if (!supportsRequiredFeatures) { + std::cout << " - Does not support required features (dynamicRendering)" << std::endl; + } + + return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && swapChainAdequate && supportsRequiredFeatures; + }); + + if (devIter != devices.end()) { + physicalDevice = *devIter; + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + std::cout << "Selected device: " << deviceProperties.deviceName << std::endl; + + // Store queue family indices for the selected device + queueFamilyIndices = findQueueFamilies(physicalDevice); + return true; + } else { + std::cerr << "Failed to find a suitable GPU. Make sure your GPU supports Vulkan and has the required extensions." << std::endl; + return false; + } + } catch (const std::exception& e) { + std::cerr << "Failed to pick physical device: " << e.what() << std::endl; + return false; + } +} + +// Create logical device +bool VulkanDevice::createLogicalDevice(bool enableValidationLayers, const std::vector& validationLayers) { + try { + // Create queue create infos for each unique queue family + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = { + queueFamilyIndices.graphicsFamily.value(), + queueFamilyIndices.presentFamily.value(), + queueFamilyIndices.computeFamily.value() + }; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + queueCreateInfos.push_back(queueCreateInfo); + } + + // Enable required features + auto features = physicalDevice.getFeatures2(); + features.features.samplerAnisotropy = vk::True; + + // Enable Vulkan 1.3 features + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + features.pNext = &vulkan13Features; + + // Create device + vk::DeviceCreateInfo createInfo{ + .pNext = &features, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + .pEnabledFeatures = nullptr // Using pNext for features + }; + + // Enable validation layers if requested + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + } + + // Create the logical device + device = vk::raii::Device(physicalDevice, createInfo); + + // Get queue handles + graphicsQueue = vk::raii::Queue(device, queueFamilyIndices.graphicsFamily.value(), 0); + presentQueue = vk::raii::Queue(device, queueFamilyIndices.presentFamily.value(), 0); + computeQueue = vk::raii::Queue(device, queueFamilyIndices.computeFamily.value(), 0); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create logical device: " << e.what() << std::endl; + return false; + } +} + +// Find queue families +QueueFamilyIndices VulkanDevice::findQueueFamilies(vk::raii::PhysicalDevice& device) { + QueueFamilyIndices indices; + + // Get queue family properties + std::vector queueFamilies = device.getQueueFamilyProperties(); + + // Find queue families that support graphics, compute, and present + for (uint32_t i = 0; i < queueFamilies.size(); i++) { + // Check for graphics support + if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) { + indices.graphicsFamily = i; + } + + // Check for compute support + if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eCompute) { + indices.computeFamily = i; + } + + // Check for present support + if (device.getSurfaceSupportKHR(i, surface)) { + indices.presentFamily = i; + } + + // If all queue families are found, break + if (indices.isComplete()) { + break; + } + } + + return indices; +} + +// Query swap chain support +SwapChainSupportDetails VulkanDevice::querySwapChainSupport(vk::raii::PhysicalDevice& device) { + SwapChainSupportDetails details; + + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(surface); + + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(surface); + + return details; +} + +// Check device extension support +bool VulkanDevice::checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device) { + // Get available extensions + std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); + + // Only check for required extensions, not optional ones + std::set requiredExtensionsSet(requiredExtensions.begin(), requiredExtensions.end()); + + // Print available extensions for debugging + std::cout << "Available extensions:" << std::endl; + for (const auto& extension : availableExtensions) { + std::cout << " " << extension.extensionName << std::endl; + requiredExtensionsSet.erase(extension.extensionName); + } + + // Print missing required extensions + if (!requiredExtensionsSet.empty()) { + std::cout << "Missing required extensions:" << std::endl; + for (const auto& extension : requiredExtensionsSet) { + std::cout << " " << extension << std::endl; + } + return false; + } + + // Check which optional extensions are supported + std::set optionalExtensionsSet(optionalExtensions.begin(), optionalExtensions.end()); + std::cout << "Supported optional extensions:" << std::endl; + for (const auto& extension : availableExtensions) { + if (optionalExtensionsSet.find(extension.extensionName) != optionalExtensionsSet.end()) { + std::cout << " " << extension.extensionName << " (supported)" << std::endl; + } + } + + return true; +} + +// Check if a device is suitable +bool VulkanDevice::isDeviceSuitable(vk::raii::PhysicalDevice& device) { + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + + // Check device extensions + bool extensionsSupported = checkDeviceExtensionSupport(device); + + // Check swap chain support + bool swapChainAdequate = false; + if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportsRequiredFeatures; +} + +// Find memory type +uint32_t VulkanDevice::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { + // Get memory properties + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + // Find suitable memory type + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); +} diff --git a/attachments/simple_engine/vulkan_device.h b/attachments/simple_engine/vulkan_device.h new file mode 100644 index 00000000..b121a824 --- /dev/null +++ b/attachments/simple_engine/vulkan_device.h @@ -0,0 +1,154 @@ +#pragma once + +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include +#include +#include +#include +#include + +/** + * @brief Structure for Vulkan queue family indices. + */ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + std::optional computeFamily; + + bool isComplete() const { + return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); + } +}; + +/** + * @brief Structure for swap chain support details. + */ +struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; + +/** + * @brief Class for managing Vulkan device selection and creation. + */ +class VulkanDevice { +public: + /** + * @brief Constructor. + * @param instance The Vulkan instance. + * @param surface The Vulkan surface. + * @param requiredExtensions The required device extensions. + * @param optionalExtensions The optional device extensions. + */ + VulkanDevice(vk::raii::Instance& instance, vk::raii::SurfaceKHR& surface, + const std::vector& requiredExtensions, + const std::vector& optionalExtensions = {}); + + /** + * @brief Destructor. + */ + ~VulkanDevice(); + + /** + * @brief Pick a suitable physical device. + * @return True if a suitable device was found, false otherwise. + */ + bool pickPhysicalDevice(); + + /** + * @brief Create a logical device. + * @param enableValidationLayers Whether to enable validation layers. + * @param validationLayers The validation layers to enable. + * @return True if the logical device was created successfully, false otherwise. + */ + bool createLogicalDevice(bool enableValidationLayers, const std::vector& validationLayers); + + /** + * @brief Get the physical device. + * @return The physical device. + */ + vk::raii::PhysicalDevice& getPhysicalDevice() { return physicalDevice; } + + /** + * @brief Get the logical device. + * @return The logical device. + */ + vk::raii::Device& getDevice() { return device; } + + /** + * @brief Get the graphics queue. + * @return The graphics queue. + */ + vk::raii::Queue& getGraphicsQueue() { return graphicsQueue; } + + /** + * @brief Get the present queue. + * @return The present queue. + */ + vk::raii::Queue& getPresentQueue() { return presentQueue; } + + /** + * @brief Get the compute queue. + * @return The compute queue. + */ + vk::raii::Queue& getComputeQueue() { return computeQueue; } + + /** + * @brief Get the queue family indices. + * @return The queue family indices. + */ + QueueFamilyIndices getQueueFamilyIndices() const { return queueFamilyIndices; } + + /** + * @brief Find queue families for a physical device. + * @param device The physical device. + * @return The queue family indices. + */ + QueueFamilyIndices findQueueFamilies(vk::raii::PhysicalDevice& device); + + /** + * @brief Query swap chain support for a physical device. + * @param device The physical device. + * @return The swap chain support details. + */ + SwapChainSupportDetails querySwapChainSupport(vk::raii::PhysicalDevice& device); + + /** + * @brief Find a memory type with the specified properties. + * @param typeFilter The type filter. + * @param properties The memory properties. + * @return The memory type index. + */ + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; + +private: + // Vulkan instance and surface + vk::raii::Instance& instance; + vk::raii::SurfaceKHR& surface; + + // Vulkan device + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + + // Vulkan queues + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + vk::raii::Queue computeQueue = nullptr; + + // Queue family indices + QueueFamilyIndices queueFamilyIndices; + + // Device extensions + std::vector requiredExtensions; + std::vector optionalExtensions; + std::vector deviceExtensions; + + // Private methods + bool isDeviceSuitable(vk::raii::PhysicalDevice& device); + bool checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device); +}; diff --git a/attachments/simple_engine/vulkan_dispatch.cpp b/attachments/simple_engine/vulkan_dispatch.cpp new file mode 100644 index 00000000..f87e64a2 --- /dev/null +++ b/attachments/simple_engine/vulkan_dispatch.cpp @@ -0,0 +1,13 @@ +#ifdef __INTELLISENSE__ +#include +#else +import vulkan_hpp; +#endif +#include + +// Define the defaultDispatchLoaderDynamic variable +namespace vk { + namespace detail { + vk::DispatchLoaderDynamic defaultDispatchLoaderDynamic; + } +} diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc new file mode 100644 index 00000000..ee450bb1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc @@ -0,0 +1,39 @@ +::pp: {plus}{plus} + += Camera & Transformations: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction + +Welcome to the "Camera & Transformations" chapter of our "Building a Simple Engine" series! In this chapter, we'll dive into the essential mathematics and techniques needed to implement a 3D camera system in Vulkan. + +Understanding how to manipulate 3D space is fundamental to creating interactive 3D applications. We'll explore the mathematical foundations of 3D transformations and implement a flexible camera system that will allow us to navigate and view our 3D scenes from any perspective. + +In this chapter, we'll focus on: + +* Understanding the mathematical foundations of 3D transformations +* Implementing different types of transformation matrices (model, view, projection) +* Creating a flexible camera system with different movement modes +* Handling user input to control the camera +* Integrating the camera system with our Vulkan rendering pipeline + +By the end of this chapter, you'll have a solid understanding of 3D transformations and a reusable camera system that can be integrated into your Vulkan applications. + +== Prerequisites + +Before starting this chapter, you should have completed the main Vulkan tutorial. You should also be familiar with: + +* Basic Vulkan concepts: +** link:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** link:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* link:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and link:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* link:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* Basic programming concepts and C++ + +link:02_math_foundations.adoc[Next: Mathematical Foundations] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc new file mode 100644 index 00000000..0c678a2d --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc @@ -0,0 +1,1080 @@ +::pp: {plus}{plus} + += Camera & Transformations: Mathematical Foundations +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Mathematical Foundations for 3D Graphics + +Before diving into camera implementation, let's review the essential mathematical concepts that form the foundation of 3D graphics programming. Understanding these concepts is crucial for implementing a robust camera system. + +=== Vectors in 3D Graphics + +Vectors are fundamental to 3D graphics as they represent positions, directions, and movements in space. In our Vulkan application, we'll primarily work with: + +* *3D vectors (x, y, z)*: Used for positions, directions, and normals +* *4D vectors (x, y, z, w)*: Used for homogeneous coordinates in transformations + +==== Why Vectors Matter in Graphics + +In our camera system, vectors serve several critical purposes: +* The camera's position is represented as a 3D vector +* The camera's viewing direction is a 3D vector +* The "up" direction that orients the camera is also a vector + +==== Vector Operations and Their Applications + +* *Addition and Subtraction*: Used for calculating relative positions and movements + - Example: `newPosition = currentPosition + movementDirection * speed` + +* *Scalar Multiplication*: Used for scaling movements and directions + - Example: Slowing down camera movement by multiplying velocity by a factor < 1 + +* *Dot Product*: Calculates the cosine of the angle between vectors (when normalized) + - Applications: Determining if objects are facing the camera, calculating lighting intensity + +* *Cross Product*: Creates a vector perpendicular to two input vectors + - Applications: Generating the camera's "right" vector from "forward" and "up" vectors + - The direction follows the right-hand rule (explained below) + +* *Normalization*: Preserves direction while setting length to 1 + - Applications: Ensuring consistent movement speed regardless of direction + +[source,cpp] +---- +// Vector operations using GLM +glm::vec3 a(1.0f, 2.0f, 3.0f); +glm::vec3 b(4.0f, 5.0f, 6.0f); + +// Addition - combining positions or offsets +glm::vec3 sum = a + b; // (5.0, 7.0, 9.0) + +// Dot product - useful for lighting calculations +float dotProduct = glm::dot(a, b); // 32.0 +// If vectors are normalized, dot product = cosine of angle between them +float cosAngle = glm::dot(glm::normalize(a), glm::normalize(b)); // ~0.974 + +// Cross product - creating perpendicular vectors (e.g., camera orientation) +glm::vec3 crossProduct = glm::cross(a, b); // (-3.0, 6.0, -3.0) + +// Normalization - ensuring consistent movement speeds +glm::vec3 normalized = glm::normalize(a); // (0.267, 0.535, 0.802) +---- + +==== The Right-Hand Rule + +The right-hand rule is a convention used in 3D graphics and mathematics to determine the orientation of coordinate systems and the direction of cross products. + +* *For Cross Products*: When calculating A × B: + 1. Point your right hand's index finger in the direction of vector A + 2. Point your middle finger in the direction of vector B (perpendicular to A) + 3. Your thumb now points in the direction of the resulting cross product + +* *For Coordinate Systems*: In a right-handed coordinate system: + 1. Point your right hand's index finger along the positive X-axis + 2. Point your middle finger along the positive Y-axis + 3. Your thumb points along the positive Z-axis + +[source,cpp] +---- +// The cross product direction follows the right-hand rule +glm::vec3 xAxis(1.0f, 0.0f, 0.0f); // Point right (positive X) +glm::vec3 yAxis(0.0f, 1.0f, 0.0f); // Point up (positive Y) + +// Cross product gives the Z axis in a right-handed system +glm::vec3 zAxis = glm::cross(xAxis, yAxis); // Points forward (positive Z) +// zAxis will be (0.0f, 0.0f, 1.0f) + +// If we reverse the order, we get the opposite direction +glm::vec3 negativeZ = glm::cross(yAxis, xAxis); // Points backward (negative Z) +// negativeZ will be (0.0f, 0.0f, -1.0f) +---- + +=== Matrices and Transformations + +Matrices are used to represent transformations in 3D space. In Vulkan and other graphics APIs, we typically use 4×4 matrices to represent transformations in homogeneous coordinates. + +==== Why We Use 4×4 Matrices + +Even though we work in 3D space, we use 4×4 matrices because: +1. They allow us to represent translation (movement) along with rotation and scaling +2. They can be combined (multiplied) to create complex transformations +3. They work with homogeneous coordinates (x, y, z, w) which are required for perspective projection + +==== Common Transformation Matrices + +* *Translation Matrix*: Moves objects in 3D space + - In a camera system: Moving the camera position + +* *Rotation Matrix*: Rotates objects around an axis + - In a camera system: Changing where the camera is looking + +* *Scale Matrix*: Changes the size of objects + - Less commonly used for cameras, but important for objects in the scene + +* *Model Matrix*: Combines transformations to position an object in world space + - Positions objects relative to the world origin + +* *View Matrix*: Transforms world space to camera space + - Essentially positions the world relative to the camera + +* *Projection Matrix*: Transforms camera space to clip space + - Defines how 3D objects are projected onto the 2D screen + - Controls perspective, field of view, and visible range (near/far planes) + +[source,cpp] +---- +// Matrix transformations using GLM +// Translation matrix - moving an object +glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(1.0f, 2.0f, 3.0f)); + +// Rotation matrix (45 degrees around Y axis) - turning an object +glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f)); + +// Scale matrix - resizing an object +glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(2.0f, 2.0f, 2.0f)); + +// Combining transformations (scale, then rotate, then translate) +// Order matters! The rightmost transformation is applied first +glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix; +---- + +==== Matrix Order Matters + +The order of matrix multiplication is crucial: +* In `A * B`, the transformation B is applied first, then A +* For our camera: `projectionMatrix * viewMatrix * modelMatrix * vertex` + +==== Row-Major vs Column-Major Representation + +When working with matrices in graphics programming, it's important to understand the difference between row-major and column-major representations: + +* *Row-Major*: Matrix elements are stored row by row in memory + - Used by DirectX, C/C++ multi-dimensional arrays + - A matrix is accessed as `M[row][column]` + +* *Column-Major*: Matrix elements are stored column by column in memory + - Used by OpenGL, GLSL, and by default in GLM + - A matrix is accessed as `M[column][row]` (in memory layout terms) + +[source,cpp] +---- +// Row-major vs Column-major representation of a 3x3 matrix +// For a matrix: +// [ a b c ] +// [ d e f ] +// [ g h i ] + +// Row-major memory layout: +// [a, b, c, d, e, f, g, h, i] + +// Column-major memory layout: +// [a, d, g, b, e, h, c, f, i] + +// In GLM, matrices are column-major by default +glm::mat4 matrix = glm::mat4(1.0f); // Identity matrix in column-major format + +// When passing matrices to Vulkan shaders, you need to be aware of the layout +// Vulkan expects column-major by default, matching GLM's default +---- + +==== Vulkan and Matrix Layouts + +Vulkan works with both row-major and column-major formats, but you need to specify which one you're using: + +* By default, Vulkan expects matrices in column-major format +* You can specify row-major format in your shaders using the `row_major` qualifier +* GLM (commonly used with Vulkan) uses column-major by default, but can be configured for row-major + +The practical implications: +* Matrix multiplication order may need to be reversed depending on the layout +* When debugging, matrix elements may appear transposed compared to mathematical notation +* When porting code between different APIs, matrix layouts may need to be transposed + +=== Affine Transformations + +Affine transformations are a fundamental concept in computer graphics that preserve parallel lines (but not necessarily angles or distances). They're essential for representing most common operations in 3D graphics. + +==== Properties of Affine Transformations + +An affine transformation can be represented as a combination of: +* Linear transformations (rotation, scaling, shearing) +* Translation (movement) + +In mathematical terms, an affine transformation can be expressed as: +f(x) = Ax + b, where A is a matrix (linear transformation) and b is a vector (translation). + +==== Why Affine Transformations Matter in Graphics + +* They preserve collinearity (points on a line remain on a line) +* They preserve ratios of distances along a line +* They can represent all the common transformations we need in 3D graphics +* They can be efficiently composed (combined) through matrix multiplication + +==== Representing Affine Transformations with Homogeneous Coordinates + +In 3D graphics, we use 4×4 matrices to represent affine transformations using homogeneous coordinates: + +[source,cpp] +---- +// A 4×4 matrix representing an affine transformation +// [ R R R Tx ] +// [ R R R Ty ] +// [ R R R Tz ] +// [ 0 0 0 1 ] +// Where R represents rotation/scaling/shearing and T represents translation + +// Example of an affine transformation matrix in GLM +glm::mat4 affineTransform = glm::mat4( + glm::vec4(r11, r12, r13, tx), // First row + glm::vec4(r21, r22, r23, ty), // Second row + glm::vec4(r31, r32, r33, tz), // Third row + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) // Last row is always (0,0,0,1) for affine transformations +); +---- + +==== Affine Transformations in Practice + +In our Vulkan application, almost all transformations we perform are affine: +* Moving objects around the scene (translation) +* Rotating objects to face different directions +* Scaling objects to make them larger or smaller +* Combining these operations to position and orient objects + +=== Pose Matrices + +A pose matrix (also called a transformation matrix or rigid body transformation) is a specific type of affine transformation that represents both the position and orientation of an object in 3D space. + +==== Structure of a Pose Matrix + +A pose matrix combines rotation and translation in a single 4×4 matrix: + +[source,cpp] +---- +// A pose matrix has this structure: +// [ R R R Tx ] +// [ R R R Ty ] +// [ R R R Tz ] +// [ 0 0 0 1 ] +// Where the 3×3 R submatrix represents rotation and [Tx,Ty,Tz] represents translation + +// Creating a pose matrix in GLM +glm::mat4 poseMatrix = glm::mat4(1.0f); // Start with identity matrix +poseMatrix = glm::translate(poseMatrix, position); // Apply translation +poseMatrix = poseMatrix * glm::mat4_cast(orientation); // Apply rotation (from quaternion) +---- + +==== Applications of Pose Matrices + +Pose matrices are essential in graphics engines for: + +* *Object Positioning*: Defining where objects are located and how they're oriented + - Example: Placing a character model in the world with the correct position and facing direction + +* *Camera Representation*: Defining the camera's position and orientation + - Example: The view matrix is the inverse of the camera's pose matrix + +* *Hierarchical Transformations*: Building complex objects from simpler parts + - Example: A character's hand position depends on the arm position, which depends on the torso position + +* *Animation*: Interpolating between different poses + - Example: Smoothly transitioning a camera from one position/orientation to another + +==== Extracting Information from Pose Matrices + +We can extract useful information from pose matrices: + +[source,cpp] +---- +// Extracting position from a pose matrix +glm::vec3 extractPosition(const glm::mat4& poseMatrix) { + return glm::vec3(poseMatrix[3]); // The translation is stored in the last column +} + +// Extracting forward direction (assuming standard OpenGL orientation) +glm::vec3 extractForwardDirection(const glm::mat4& poseMatrix) { + return -glm::vec3(poseMatrix[2]); // Negative Z axis (third column) +} + +// Extracting up direction +glm::vec3 extractUpDirection(const glm::mat4& poseMatrix) { + return glm::vec3(poseMatrix[1]); // Y axis (second column) +} +---- + +=== Implementing a Look-At Function + +A "look-at" function is a fundamental tool in camera systems that creates a view matrix to orient the camera towards a specific target point. This is one of the most common operations in 3D graphics and provides an excellent example of how the mathematical concepts we've discussed are applied in practice. + +==== Purpose of the Look-At Function + +The look-at function serves several important purposes: + +* Orients the camera to face a specific point in 3D space +* Establishes the camera's local coordinate system (right, up, forward vectors) +* Creates a view matrix that transforms world coordinates into camera space +* Simplifies camera control by focusing on a target rather than managing rotation angles + +==== Mathematical Principles + +The look-at function works by constructing an orthonormal basis (three perpendicular unit vectors) that defines the camera's orientation: + +1. *Forward Vector (Z)*: Points from the camera position to the target position +2. *Right Vector (X)*: Perpendicular to both the forward vector and the world up vector +3. *Up Vector (Y)*: Perpendicular to both the forward and right vectors + +These three vectors, along with the camera position, form the view matrix that transforms world coordinates into camera space. + +==== Step-by-Step Implementation + +Let's implement a custom look-at function to understand how it works: + +[source,cpp] +---- +glm::mat4 createLookAtMatrix( + const glm::vec3& cameraPosition, // Where the camera is + const glm::vec3& targetPosition, // What the camera is looking at + const glm::vec3& worldUpVector // Which way is "up" (usually Y axis) +) { + // Step 1: Calculate the camera's forward direction (Z axis) + // Note: We negate this because in OpenGL/Vulkan, the camera looks down the negative Z-axis + glm::vec3 forward = glm::normalize(cameraPosition - targetPosition); + + // Step 2: Calculate the camera's right direction (X axis) + // Using cross product between world up and forward direction + glm::vec3 right = glm::normalize(glm::cross(worldUpVector, forward)); + + // Step 3: Calculate the camera's up direction (Y axis) + // Using cross product between forward and right to ensure orthogonality + glm::vec3 up = glm::cross(forward, right); + + // Step 4: Construct the rotation part of the view matrix + // Each row contains one of the camera's basis vectors + glm::mat4 rotation = glm::mat4(1.0f); + rotation[0][0] = right.x; + rotation[1][0] = right.y; + rotation[2][0] = right.z; + rotation[0][1] = up.x; + rotation[1][1] = up.y; + rotation[2][1] = up.z; + rotation[0][2] = forward.x; + rotation[1][2] = forward.y; + rotation[2][2] = forward.z; + + // Step 5: Construct the translation part of the view matrix + glm::mat4 translation = glm::mat4(1.0f); + translation[3][0] = -cameraPosition.x; + translation[3][1] = -cameraPosition.y; + translation[3][2] = -cameraPosition.z; + + // Step 6: Combine rotation and translation + // The translation is applied first, then the rotation + return rotation * translation; +} +---- + +==== Using GLM's Built-in Look-At Function + +In practice, we typically use GLM's built-in `lookAt` function, which implements the same algorithm: + +[source,cpp] +---- +// Using GLM's built-in lookAt function +glm::mat4 viewMatrix = glm::lookAt( + glm::vec3(0.0f, 0.0f, 5.0f), // Camera position + glm::vec3(0.0f, 0.0f, 0.0f), // Target position (origin) + glm::vec3(0.0f, 1.0f, 0.0f) // World up vector (Y axis) +); +---- + +==== Practical Applications + +The look-at function is used in various scenarios: + +* *First-Person Camera*: Looking in the direction of movement +* *Third-Person Camera*: Following a character while looking at them +* *Orbit Camera*: Circling around a point of interest +* *Cinematic Camera*: Creating smooth camera movements that focus on important objects +* *Object Inspection*: Allowing users to examine 3D models from different angles + +==== Example: Implementing an Orbit Camera + +Here's how you might use the look-at function to implement an orbit camera that circles around a target: + +[source,cpp] +---- +// Orbit camera implementation +void updateOrbitCamera(float deltaTime) { + // Update the orbit angle based on time + orbitAngle += orbitSpeed * deltaTime; + + // Calculate the camera position on a circle around the target + float radius = 10.0f; + glm::vec3 cameraPosition( + targetPosition.x + radius * cos(orbitAngle), + targetPosition.y + 5.0f, // Slightly above the target + targetPosition.z + radius * sin(orbitAngle) + ); + + // Create the view matrix using lookAt + viewMatrix = glm::lookAt( + cameraPosition, + targetPosition, + glm::vec3(0.0f, 1.0f, 0.0f) + ); +} +---- + +==== Example: Smooth Camera Transitions + +The look-at function can also be used to create smooth transitions between different camera positions and targets: + +[source,cpp] +---- +// Smooth camera transition +void transitionCamera(float t) { // t ranges from 0.0 to 1.0 + // Interpolate between start and end positions + glm::vec3 currentPosition = glm::mix(startPosition, endPosition, t); + + // Interpolate between start and end targets + glm::vec3 currentTarget = glm::mix(startTarget, endTarget, t); + + // Update the view matrix + viewMatrix = glm::lookAt( + currentPosition, + currentTarget, + glm::vec3(0.0f, 1.0f, 0.0f) + ); +} +---- + +By understanding how the look-at function works, you gain insight into how cameras are oriented in 3D space and how the view matrix transforms the world from the camera's perspective. + +=== Raycasting in 3D Graphics + +Raycasting is a fundamental technique in 3D graphics that involves projecting rays from a point into the scene and determining what they intersect with. It's used for a wide range of applications, from picking objects in a scene to implementing collision detection and visibility determination. + +==== Ray Representation + +A ray in 3D space is defined by an origin point and a direction vector: + +[source,cpp] +---- +struct Ray { + glm::vec3 origin; // Starting point of the ray + glm::vec3 direction; // Normalized direction vector +}; + +// Creating a ray +Ray createRay(const glm::vec3& origin, const glm::vec3& direction) { + Ray ray; + ray.origin = origin; + ray.direction = glm::normalize(direction); // Ensure direction is normalized + return ray; +} +---- + +==== Ray-Object Intersection + +The core of raycasting is determining if and where a ray intersects with objects in the scene. Let's look at some common intersection tests: + +===== Ray-Sphere Intersection + +One of the simplest intersection tests is between a ray and a sphere: + +[source,cpp] +---- +struct Sphere { + glm::vec3 center; + float radius; +}; + +bool rayIntersectsSphere(const Ray& ray, const Sphere& sphere, float& t) { + // Vector from ray origin to sphere center + glm::vec3 oc = ray.origin - sphere.center; + + // Quadratic equation coefficients + float a = glm::dot(ray.direction, ray.direction); // Always 1 if direction is normalized + float b = 2.0f * glm::dot(oc, ray.direction); + float c = glm::dot(oc, oc) - sphere.radius * sphere.radius; + + // Discriminant + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + // No intersection + return false; + } + + // Find the nearest intersection point + float sqrtDiscriminant = sqrt(discriminant); + float t0 = (-b - sqrtDiscriminant) / (2 * a); + float t1 = (-b + sqrtDiscriminant) / (2 * a); + + // Check if intersection is in front of the ray + if (t0 > 0) { + t = t0; + return true; + } + + if (t1 > 0) { + t = t1; + return true; + } + + // Both intersections are behind the ray + return false; +} +---- + +===== Ray-Triangle Intersection + +Triangle intersection is essential for raycasting against 3D models: + +[source,cpp] +---- +struct Triangle { + glm::vec3 v0, v1, v2; // Vertices +}; + +bool rayIntersectsTriangle(const Ray& ray, const Triangle& triangle, float& t, glm::vec2& barycentricCoords) { + // Möller–Trumbore algorithm + glm::vec3 edge1 = triangle.v1 - triangle.v0; + glm::vec3 edge2 = triangle.v2 - triangle.v0; + glm::vec3 h = glm::cross(ray.direction, edge2); + float a = glm::dot(edge1, h); + + // Check if ray is parallel to triangle + if (a > -0.00001f && a < 0.00001f) { + return false; + } + + float f = 1.0f / a; + glm::vec3 s = ray.origin - triangle.v0; + float u = f * glm::dot(s, h); + + // Check if intersection is outside triangle + if (u < 0.0f || u > 1.0f) { + return false; + } + + glm::vec3 q = glm::cross(s, edge1); + float v = f * glm::dot(ray.direction, q); + + // Check if intersection is outside triangle + if (v < 0.0f || u + v > 1.0f) { + return false; + } + + // Compute intersection distance + t = f * glm::dot(edge2, q); + + // Check if intersection is behind the ray + if (t <= 0.0f) { + return false; + } + + // Store barycentric coordinates for interpolation + barycentricCoords = glm::vec2(u, v); + return true; +} +---- + +===== Ray-AABB Intersection + +Axis-Aligned Bounding Box (AABB) intersection is useful for broad-phase collision detection: + +[source,cpp] +---- +struct AABB { + glm::vec3 min; // Minimum corner + glm::vec3 max; // Maximum corner +}; + +bool rayIntersectsAABB(const Ray& ray, const AABB& aabb, float& tMin, float& tMax) { + // Compute intersection with each slab + glm::vec3 invDir = 1.0f / ray.direction; + glm::vec3 t0 = (aabb.min - ray.origin) * invDir; + glm::vec3 t1 = (aabb.max - ray.origin) * invDir; + + // Handle negative directions + glm::vec3 tSmaller = glm::min(t0, t1); + glm::vec3 tBigger = glm::max(t0, t1); + + // Find entry and exit points + tMin = glm::max(tSmaller.x, glm::max(tSmaller.y, tSmaller.z)); + tMax = glm::min(tBigger.x, glm::min(tBigger.y, tBigger.z)); + + // Check if there's a valid intersection + return tMax >= tMin && tMax > 0; +} +---- + +==== Creating Camera Rays + +One of the most common uses of raycasting is to create rays from the camera into the scene, which is essential for picking objects or implementing ray tracing: + +[source,cpp] +---- +Ray createCameraRay( + const glm::vec2& screenCoord, // Normalized screen coordinates (-1 to 1) + const glm::mat4& viewMatrix, // Camera view matrix + const glm::mat4& projectionMatrix // Camera projection matrix +) { + // Convert to clip space + glm::vec4 clipCoords(screenCoord.x, screenCoord.y, -1.0f, 1.0f); + + // Convert to view space + glm::mat4 invProjection = glm::inverse(projectionMatrix); + glm::vec4 viewCoords = invProjection * clipCoords; + viewCoords.z = -1.0f; // Point towards negative Z in view space + viewCoords.w = 0.0f; // Convert to direction vector + + // Convert to world space + glm::mat4 invView = glm::inverse(viewMatrix); + glm::vec4 worldCoords = invView * viewCoords; + + // Create ray + Ray ray; + ray.origin = glm::vec3(invView[3]); // Camera position in world space + ray.direction = glm::normalize(glm::vec3(worldCoords)); + + return ray; +} +---- + +==== Applications of Raycasting in Graphics + +Raycasting has numerous applications in 3D graphics and game development: + +* *Object Picking*: Determining which object the user clicked on in a 3D scene + - Cast a ray from the camera through the mouse position and find the nearest intersection + +* *Collision Detection*: Checking if objects will collide along a movement path + - Cast rays in the direction of movement to detect potential collisions + +* *Line of Sight*: Determining if one object can "see" another + - Cast a ray between two objects and check for obstructions + +* *Terrain Height Sampling*: Finding the height of terrain at a specific point + - Cast a ray downward from above the terrain + +* *Physics Simulations*: Implementing realistic physics behaviors + - Raycasting is fundamental to many physics engines for collision resolution + +* *AI Navigation*: Helping AI characters navigate environments + - Raycasting can detect obstacles and determine valid paths + +==== Optimizing Raycasting Performance + +For complex scenes with many objects, raycasting can become computationally expensive. Here are some optimization techniques: + +* *Spatial Partitioning*: Use data structures like octrees, BVHs, or k-d trees to quickly eliminate objects that can't possibly intersect with the ray + +* *Bounding Volume Hierarchies*: Test against simple bounding volumes (spheres, AABBs) before performing more expensive tests against detailed geometry + +* *Level of Detail*: Use simpler collision geometry for distant objects + +* *Ray Batching*: Process multiple rays together to take advantage of SIMD instructions + +* *Early Termination*: Stop testing once you've found the closest intersection (if that's all you need) + +=== Projection in 3D Graphics + +Projection is the process of transforming 3D coordinates in view space to 2D coordinates on the screen. In computer graphics, we use projection matrices to perform this transformation. + +==== Types of Projection + +There are two main types of projection used in 3D graphics: + +* *Perspective Projection*: Objects appear smaller as they get farther away, simulating how we see the world +* *Orthographic Projection*: Objects maintain their size regardless of distance, useful for technical drawings, 2D games, and UI elements + +==== Perspective Projection + +Perspective projection creates a realistic view where distant objects appear smaller, creating the illusion of depth: + +[source,cpp] +---- +// Creating a perspective projection matrix +glm::mat4 createPerspectiveMatrix( + float fovY, // Vertical field of view in degrees + float aspectRatio, // Width / height of the viewport + float nearPlane, // Distance to the near clipping plane + float farPlane // Distance to the far clipping plane +) { + return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); +} +---- + +The perspective projection matrix performs several transformations: + +1. Scales the view frustum based on the field of view and aspect ratio +2. Maps the view volume to a canonical view volume (a cube from -1 to 1 in each dimension) +3. Applies perspective division (dividing by w) to create the perspective effect + +The resulting matrix has this structure: + +[source,cpp] +---- +// Structure of a perspective projection matrix +// [ (h/w)*cot(fovY/2) 0 0 0 ] +// [ 0 cot(fovY/2) 0 0 ] +// [ 0 0 -(f+n)/(f-n) -2*f*n/(f-n) ] +// [ 0 0 -1 0 ] +// Where: +// - fovY is the vertical field of view +// - w/h is the aspect ratio +// - n is the near plane distance +// - f is the far plane distance +---- + +==== Orthographic Projection + +Orthographic projection maintains the size of objects regardless of their distance from the camera: + +[source,cpp] +---- +// Creating an orthographic projection matrix +glm::mat4 createOrthographicMatrix( + float left, // Left plane coordinate + float right, // Right plane coordinate + float bottom, // Bottom plane coordinate + float top, // Top plane coordinate + float nearPlane, // Near plane distance + float farPlane // Far plane distance +) { + return glm::ortho(left, right, bottom, top, nearPlane, farPlane); +} +---- + +The orthographic projection matrix simply scales and translates the view volume to the canonical view volume without applying any perspective division: + +[source,cpp] +---- +// Structure of an orthographic projection matrix +// [ 2/(r-l) 0 0 -(r+l)/(r-l) ] +// [ 0 2/(t-b) 0 -(t+b)/(t-b) ] +// [ 0 0 -2/(f-n) -(f+n)/(f-n) ] +// [ 0 0 0 1 ] +// Where: +// - l, r are the left and right planes +// - b, t are the bottom and top planes +// - n, f are the near and far planes +---- + +==== The View Frustum + +The view frustum is the volume of space visible to the camera. For perspective projection, it's a truncated pyramid: + +* *Near Plane*: The closest plane to the camera where rendering begins +* *Far Plane*: The farthest plane from the camera where rendering ends +* *Field of View (FOV)*: The angle that determines how wide the view is +* *Aspect Ratio*: The ratio of width to height of the viewport + +[source,cpp] +---- +// Calculating the corners of the view frustum +void calculateFrustumCorners( + float fovY, + float aspectRatio, + float nearPlane, + float farPlane, + glm::vec3 corners[8] // Output array for the 8 corners +) { + float tanHalfFovY = tan(glm::radians(fovY) / 2.0f); + + // Near plane dimensions + float nearHeight = 2.0f * nearPlane * tanHalfFovY; + float nearWidth = nearHeight * aspectRatio; + + // Far plane dimensions + float farHeight = 2.0f * farPlane * tanHalfFovY; + float farWidth = farHeight * aspectRatio; + + // Near plane corners (in view space) + corners[0] = glm::vec3(-nearWidth/2, -nearHeight/2, -nearPlane); // Bottom-left + corners[1] = glm::vec3( nearWidth/2, -nearHeight/2, -nearPlane); // Bottom-right + corners[2] = glm::vec3( nearWidth/2, nearHeight/2, -nearPlane); // Top-right + corners[3] = glm::vec3(-nearWidth/2, nearHeight/2, -nearPlane); // Top-left + + // Far plane corners (in view space) + corners[4] = glm::vec3(-farWidth/2, -farHeight/2, -farPlane); // Bottom-left + corners[5] = glm::vec3( farWidth/2, -farHeight/2, -farPlane); // Bottom-right + corners[6] = glm::vec3( farWidth/2, farHeight/2, -farPlane); // Top-right + corners[7] = glm::vec3(-farWidth/2, farHeight/2, -farPlane); // Top-left +} +---- + +==== Projection and Unprojection + +Projection converts 3D world coordinates to 2D screen coordinates, while unprojection does the reverse: + +[source,cpp] +---- +// Project a 3D point to screen space +glm::vec2 projectPoint( + const glm::vec3& worldPoint, + const glm::mat4& viewMatrix, + const glm::mat4& projectionMatrix, + const glm::vec4& viewport // (x, y, width, height) +) { + // Transform to clip space + glm::vec4 clipSpace = projectionMatrix * viewMatrix * glm::vec4(worldPoint, 1.0f); + + // Perspective division + glm::vec3 ndcSpace = glm::vec3(clipSpace) / clipSpace.w; + + // Map to viewport + glm::vec2 screenPos; + screenPos.x = (ndcSpace.x + 1.0f) * 0.5f * viewport.z + viewport.x; + screenPos.y = (1.0f - ndcSpace.y) * 0.5f * viewport.w + viewport.y; // Y is flipped + + return screenPos; +} + +// Unproject a screen point to a ray in world space +Ray unprojectScreenPoint( + const glm::vec2& screenPoint, + const glm::mat4& viewMatrix, + const glm::mat4& projectionMatrix, + const glm::vec4& viewport // (x, y, width, height) +) { + // Convert to normalized device coordinates + glm::vec3 ndcPos; + ndcPos.x = 2.0f * (screenPoint.x - viewport.x) / viewport.z - 1.0f; + ndcPos.y = 1.0f - 2.0f * (screenPoint.y - viewport.y) / viewport.w; // Y is flipped + ndcPos.z = -1.0f; // Near plane + + // Create ray from camera through this point + return createCameraRay(glm::vec2(ndcPos.x, ndcPos.y), viewMatrix, projectionMatrix); +} +---- + +==== Applications of Projection in Graphics + +Projection matrices are used in various ways in 3D graphics: + +* *Rendering*: Converting 3D scene geometry to 2D screen pixels +* *Shadow Mapping*: Projecting the scene from a light's perspective to determine shadows +* *Reflection/Refraction*: Calculating how light bounces off or passes through surfaces +* *Texture Projection*: Mapping textures onto surfaces based on a projector's perspective +* *Screen-Space Effects*: Implementing post-processing effects like screen-space reflections or ambient occlusion + +==== Choosing the Right Projection + +The choice between perspective and orthographic projection depends on the application: + +* *Use Perspective Projection for*: + - First-person or third-person games + - Realistic 3D visualizations + - Any application where depth perception is important + +* *Use Orthographic Projection for*: + - 2D games with 3D elements + - Technical drawings and CAD applications + - UI elements that shouldn't be affected by perspective + - Isometric or top-down games + +=== Quaternions for Rotations + +While rotation matrices work well, quaternions offer advantages for certain rotation operations, particularly for smooth camera movements and avoiding "gimbal lock" (loss of a degree of freedom in certain orientations). + +==== Why Use Quaternions? + +* More compact representation (4 components vs. 9 for a rotation matrix) +* Easier to interpolate smoothly between orientations (important for camera animations) +* Avoids gimbal lock issues that can occur with Euler angles (pitch, yaw, roll) + +[source,cpp] +---- +// Quaternion operations using GLM +// Create a quaternion from Euler angles (in radians) +glm::quat rotation = glm::quat(glm::vec3( + glm::radians(30.0f), // pitch (X) - looking up/down + glm::radians(45.0f), // yaw (Y) - looking left/right + glm::radians(60.0f) // roll (Z) - tilting the camera +)); + +// Convert quaternion to rotation matrix for use in rendering +glm::mat4 rotationMatrix = glm::mat4_cast(rotation); + +// Rotate a vector using a quaternion (e.g., rotating the camera's forward vector) +glm::vec3 original(1.0f, 0.0f, 0.0f); +glm::vec3 rotated = rotation * original; +---- + +=== Coordinate Systems in 3D Graphics + +Understanding the different coordinate systems is essential for implementing a camera system. As data moves through the rendering pipeline, it undergoes several transformations: + +* *Local Space (Object Space)*: Coordinates relative to the object's origin + - Where vertices are initially defined relative to their own object + +* *World Space*: Coordinates relative to the world origin + - Where objects are positioned relative to each other in the scene + +* *View Space (Camera Space)*: Coordinates relative to the camera + - The world as seen from the camera's position and orientation + - The camera is at the origin (0,0,0) looking down the negative Z-axis + +* *Clip Space*: Coordinates after projection, in the range [-w, w] for each axis + - Determines what's visible on screen (inside the view frustum) + +* *Screen Space*: Final 2D coordinates for display on the screen + - The actual pixel positions where objects appear + +==== Handedness of Coordinate Systems + +Graphics APIs and engines use either right-handed or left-handed coordinate systems: + +* *Right-Handed System* (used by OpenGL and Vulkan by convention): + - X-axis points right + - Y-axis points up + - Z-axis points out of the screen (toward the viewer) + - Cross product: Z = X × Y (using the right-hand rule) + +* *Left-Handed System* (used by DirectX): + - X-axis points right + - Y-axis points up + - Z-axis points into the screen (away from the viewer) + - Cross product: Z = X × Y (using the left-hand rule) + +[source,cpp] +---- +// In Vulkan, we typically use a right-handed coordinate system +// But we can convert between systems if needed + +// Converting a point from left-handed to right-handed system +// (just flip the Z coordinate) +glm::vec3 leftHandedPoint(x, y, z); +glm::vec3 rightHandedPoint(x, y, -z); + +// When setting up a camera, the handedness affects the view matrix +// In a right-handed system, the camera typically looks down the negative Z-axis +// This is why we often see -Z as the "forward" direction in camera code +---- + +==== Implications for Camera Systems + +The handedness of your coordinate system affects how you set up your camera: + +* In a right-handed system (Vulkan convention): + - The camera typically looks down the negative Z-axis + - The "look" vector is often stored as a negative Z direction + - The view matrix is constructed using the right-hand rule for cross products + +* When extracting axes from a view matrix: + - Right vector: X-axis of the view matrix + - Up vector: Y-axis of the view matrix + - Forward vector: Negative Z-axis of the view matrix + +==== The Transformation Pipeline + +The transformation pipeline typically follows this sequence: +Local Space → World Space → View Space → Clip Space → Screen Space + +[source,cpp] +---- +// A typical vertex transformation in a shader +gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); +---- + +In the next section, we'll implement these mathematical concepts to create a flexible camera system for our Vulkan application. + +=== Further Resources + +If you're finding some of the mathematical concepts challenging or want to deepen your understanding, here are some helpful resources organized by topic: + +==== General 3D Math Resources + +* *Books*: + - "Mathematics for 3D Game Programming and Computer Graphics" by Eric Lengyel - Comprehensive reference for 3D math + - "3D Math Primer for Graphics and Game Development" by Fletcher Dunn and Ian Parberry - Excellent beginner-friendly introduction + - "Essential Mathematics for Games and Interactive Applications" by James M. Van Verth and Lars M. Bishop - Practical approach with code examples + +* *Online Courses*: + - https://www.khanacademy.org/math/linear-algebra[Khan Academy Linear Algebra] - Free course covering vector and matrix fundamentals + - https://www.coursera.org/learn/linear-algebra-machine-learning[Mathematics for Machine Learning: Linear Algebra] - Covers vectors, matrices, and transformations + +* *Interactive Tools*: + - https://eater.net/quaternions[Quaternion Visualizer] - Interactive visualization of quaternion rotations + - https://math.hws.edu/graphicsbook/c3/s5.html[Interactive 3D Transformations] - Experiment with different transformations + +==== Vectors and Vector Operations + +* *Tutorials*: + - https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/vectors.html[Scratchapixel: Vectors] - Detailed explanation with graphics + - https://www.youtube.com/watch?v=fNk_zzaMoSs&list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab[3Blue1Brown: Essence of Linear Algebra] - Excellent visual explanations of vectors + +* *Interactive Tools*: + - https://www.geogebra.org/m/qCHzkpXh[GeoGebra: Vector Operations] - Interactive vector addition, subtraction, dot and cross products + - https://www.falstad.com/dotproduct/[Dot Product Visualization] - Interactive visualization of dot products + +==== Matrices and Transformations + +* *Tutorials*: + - https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/transformations.html[Scratchapixel: Transformations] - Detailed explanation of transformation matrices + - https://learnopengl.com/Getting-started/Transformations[LearnOpenGL: Transformations] - Practical guide to transformations in graphics + +* *Interactive Tools*: + - https://www.shadertoy.com/view/ltBXW3[ShaderToy: Matrix Transformations] - Interactive visualization of matrix transformations + - https://www.redblobgames.com/articles/transform/[Red Blob Games: Interactive Transformations] - Visual explanation of 2D transformations (concepts extend to 3D) + +==== Quaternions + +* *Tutorials*: + - https://www.youtube.com/watch?v=zjMuIxRvygQ[3Blue1Brown: Quaternions and 3D rotation] - Visual explanation of quaternions + - https://www.3dgep.com/understanding-quaternions/[Understanding Quaternions] - Practical guide with code examples + +* *Interactive Tools*: + - https://eater.net/quaternions[Quaternion Visualizer] - Interactive visualization of quaternion rotations + - https://www.shadertoy.com/view/lsl3RH[ShaderToy: Quaternion Rotation] - Interactive quaternion rotation visualization + +==== Coordinate Systems and Handedness + +* *Tutorials*: + - https://learnopengl.com/Getting-started/Coordinate-Systems[LearnOpenGL: Coordinate Systems] - Explanation of different coordinate systems in graphics + - https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/coordinate-systems.html[Scratchapixel: Coordinate Systems] - Detailed explanation with graphics + +* *References*: + - https://www.khronos.org/opengl/wiki/Coordinate_Transformations[OpenGL Wiki: Coordinate Transformations] - Reference for coordinate transformations + - https://docs.microsoft.com/en-us/windows/win32/direct3d9/coordinate-systems[Microsoft Docs: Coordinate Systems] - Explanation of left-handed vs. right-handed systems + +==== Vulkan-Specific Resources + +* *Official Documentation*: + - https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html[Vulkan Specification] - Official reference (see sections on coordinate systems) + - https://github.com/KhronosGroup/Vulkan-Guide[Khronos Vulkan Guide] - Official guide with explanations of Vulkan concepts + +* *Tutorials*: + - https://vkguide.dev/[Vulkan Guide] - Modern Vulkan tutorial with explanations of math concepts + +==== GLM Library (Used in our examples) + +* *Documentation*: + - https://github.com/g-truc/glm/blob/master/manual.md[GLM Manual] - Official documentation for the GLM math library + - https://glm.g-truc.net/0.9.9/api/index.html[GLM API Documentation] - API reference + +* *Tutorials*: + - https://learnopengl.com/Getting-started/Transformations[LearnOpenGL: Transformations with GLM] - Practical guide to using GLM for transformations + - https://www.lighthouse3d.com/tutorials/glm-tutorial/[GLM Tutorial] - Tutorial on using GLM for graphics math + +==== Interactive Learning Tools + +* *Visualizations*: + - https://www.geogebra.org/3d[GeoGebra 3D Calculator] - Create and manipulate 3D objects and transformations + - https://www.shadertoy.com/[ShaderToy] - Experiment with shaders that use 3D math + +* *Practice Problems*: + - https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces[Khan Academy: Vectors and Spaces] - Practice problems for vector math + - https://www.khanacademy.org/math/linear-algebra/matrix-transformations[Khan Academy: Matrix Transformations] - Practice problems for matrix transformations + +These resources should help you gain a deeper understanding of the mathematical concepts used in 3D graphics and camera systems. If you're struggling with a particular concept, try looking at multiple resources as different explanations might resonate better with your learning style. + +link:03_camera_implementation.adoc[Next: Camera Implementation] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc new file mode 100644 index 00000000..e80a41b2 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc @@ -0,0 +1,434 @@ +::pp: {plus}{plus} + += Camera & Transformations: Camera Implementation +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Camera Implementation + +Now that we understand the mathematical foundations, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. + +=== Camera Types + +There are several types of cameras commonly used in 3D applications: + +* *First-Person Camera*: Simulates viewing the world through the eyes of a character. +* *Third-Person Camera*: Follows a character from behind or another fixed position. +* *Orbit Camera*: Rotates around a fixed point, useful for object inspection. +* *Free Camera*: Allows unrestricted movement in all directions. + +For our implementation, we'll focus on a versatile camera that can be configured for different use cases. + +=== Camera Class Design + +Let's design a camera class that encapsulates the necessary functionality: + +[source,cpp] +---- +class Camera { +private: + // Camera position and orientation + glm::vec3 position; + glm::vec3 front; + glm::vec3 up; + glm::vec3 right; + glm::vec3 worldUp; + + // Euler angles + float yaw; + float pitch; + + // Camera options + float movementSpeed; + float mouseSensitivity; + float zoom; + + // Update camera vectors based on Euler angles + void updateCameraVectors(); + +public: + // Constructor with default values + Camera( + glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), + float yaw = -90.0f, + float pitch = 0.0f + ); + + // Get view matrix + glm::mat4 getViewMatrix() const; + + // Get projection matrix + glm::mat4 getProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 100.0f) const; + + // Process keyboard input for camera movement + void processKeyboard(CameraMovement direction, float deltaTime); + + // Process mouse movement for camera rotation + void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); + + // Process mouse scroll for zoom + void processMouseScroll(float yOffset); + + // Getters for camera properties + glm::vec3 getPosition() const { return position; } + glm::vec3 getFront() const { return front; } + float getZoom() const { return zoom; } +}; +---- + +=== Camera Movement + +We'll define an enum for camera movement directions: + +[source,cpp] +---- +enum class CameraMovement { + FORWARD, + BACKWARD, + LEFT, + RIGHT, + UP, + DOWN +}; +---- + +And implement the movement logic: + +[source,cpp] +---- +void Camera::processKeyboard(CameraMovement direction, float deltaTime) { + float velocity = movementSpeed * deltaTime; + + if (direction == CameraMovement::FORWARD) + position += front * velocity; + if (direction == CameraMovement::BACKWARD) + position -= front * velocity; + if (direction == CameraMovement::LEFT) + position -= right * velocity; + if (direction == CameraMovement::RIGHT) + position += right * velocity; + if (direction == CameraMovement::UP) + position += up * velocity; + if (direction == CameraMovement::DOWN) + position -= up * velocity; +} +---- + +=== Camera Rotation + +For camera rotation, we'll use mouse input to adjust the yaw and pitch angles: + +[source,cpp] +---- +void Camera::processMouseMovement(float xOffset, float yOffset, bool constrainPitch) { + xOffset *= mouseSensitivity; + yOffset *= mouseSensitivity; + + yaw += xOffset; + pitch += yOffset; + + // Constrain pitch to avoid flipping + if (constrainPitch) { + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + } + + // Update camera vectors based on updated Euler angles + updateCameraVectors(); +} +---- + +=== Updating Camera Vectors + +After changing the camera's orientation, we need to recalculate the front, right, and up vectors: + +[source,cpp] +---- +void Camera::updateCameraVectors() { + // Calculate the new front vector + glm::vec3 newFront; + newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); + newFront.y = sin(glm::radians(pitch)); + newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); + front = glm::normalize(newFront); + + // Recalculate the right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +=== View Matrix + +The view matrix transforms world coordinates into view coordinates (camera space): + +[source,cpp] +---- +glm::mat4 Camera::getViewMatrix() const { + return glm::lookAt(position, position + front, up); +} +---- + +=== Projection Matrix + +The projection matrix transforms view coordinates into clip coordinates: + +[source,cpp] +---- +glm::mat4 Camera::getProjectionMatrix(float aspectRatio, float nearPlane, float farPlane) const { + return glm::perspective(glm::radians(zoom), aspectRatio, nearPlane, farPlane); +} +---- + +=== Advanced Topics: Third-Person Camera Implementation + +In this section, we'll explore advanced techniques for implementing a third-person camera that follows a character while avoiding occlusion and maintaining focus on the character. + +==== Third-Person Camera Design + +A third-person camera typically needs to: + +1. Follow the character at a specified distance +2. Maintain a consistent view of the character +3. Avoid being occluded by objects in the environment +4. Provide smooth transitions during movement and rotation + +Let's extend our camera class to support these features: + +[source,cpp] +---- +class ThirdPersonCamera : public Camera { +private: + // Target (character) properties + glm::vec3 targetPosition; + glm::vec3 targetForward; + + // Camera configuration + float followDistance; + float followHeight; + float followSmoothness; + + // Occlusion avoidance + float minDistance; + float raycastDistance; + + // Internal state + glm::vec3 desiredPosition; + glm::vec3 smoothDampVelocity; + +public: + ThirdPersonCamera( + float followDistance = 5.0f, + float followHeight = 2.0f, + float followSmoothness = 0.1f, + float minDistance = 1.0f + ); + + // Update camera position based on target + void updatePosition(const glm::vec3& targetPos, const glm::vec3& targetFwd, float deltaTime); + + // Handle occlusion avoidance + void handleOcclusion(const Scene& scene); + + // Orbit around target + void orbit(float horizontalAngle, float verticalAngle); + + // Setters for camera properties + void setFollowDistance(float distance) { followDistance = distance; } + void setFollowHeight(float height) { followHeight = height; } + void setFollowSmoothness(float smoothness) { followSmoothness = smoothness; } +}; +---- + +==== Character Following Algorithm + +The core of a third-person camera is the algorithm that positions the camera relative to the character. Here's an implementation of the `updatePosition` method: + +[source,cpp] +---- +void ThirdPersonCamera::updatePosition( + const glm::vec3& targetPos, + const glm::vec3& targetFwd, + float deltaTime +) { + // Update target properties + targetPosition = targetPos; + targetForward = glm::normalize(targetFwd); + + // Calculate the desired camera position + // Position the camera behind and above the character + glm::vec3 offset = -targetForward * followDistance; + offset.y = followHeight; + + desiredPosition = targetPosition + offset; + + // Smooth camera movement using exponential smoothing + position = glm::mix(position, desiredPosition, 1.0f - pow(followSmoothness, deltaTime * 60.0f)); + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + + // Recalculate right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Positions the camera behind the character based on the character's forward direction +2. Adds height to give a better view of the character and surroundings +3. Uses exponential smoothing to create natural camera movement +4. Always keeps the camera focused on the character + +==== Occlusion Avoidance + +One of the most challenging aspects of a third-person camera is handling occlusion - when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: + +[source,cpp] +---- +void ThirdPersonCamera::handleOcclusion(const Scene& scene) { + // Cast a ray from the target to the desired camera position + Ray ray; + ray.origin = targetPosition; + ray.direction = glm::normalize(desiredPosition - targetPosition); + + // Check for intersections with scene objects + RaycastHit hit; + if (scene.raycast(ray, hit, glm::length(desiredPosition - targetPosition))) { + // If there's an intersection, move the camera to the hit point + // minus a small offset to avoid clipping + float offsetDistance = 0.2f; + position = hit.point - (ray.direction * offsetDistance); + + // Ensure we don't get too close to the target + float currentDistance = glm::length(position - targetPosition); + if (currentDistance < minDistance) { + position = targetPosition + ray.direction * minDistance; + } + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); + } +} +---- + +This implementation: + +1. Casts a ray from the character to the desired camera position +2. If the ray hits an object, moves the camera to the hit point (with a small offset) +3. Ensures the camera doesn't get too close to the character +4. Updates the camera orientation to maintain focus on the character + +==== Implementing Orbit Controls + +Many third-person games allow the player to orbit the camera around the character. Here's how to implement this functionality: + +[source,cpp] +---- +void ThirdPersonCamera::orbit(float horizontalAngle, float verticalAngle) { + // Update yaw and pitch based on input + yaw += horizontalAngle; + pitch += verticalAngle; + + // Constrain pitch to avoid flipping + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + + // Calculate the new camera position based on spherical coordinates + float radius = followDistance; + float yawRad = glm::radians(yaw); + float pitchRad = glm::radians(pitch); + + // Convert spherical coordinates to Cartesian + glm::vec3 offset; + offset.x = radius * cos(yawRad) * cos(pitchRad); + offset.y = radius * sin(pitchRad); + offset.z = radius * sin(yawRad) * cos(pitchRad); + + // Set the desired position + desiredPosition = targetPosition + offset; + + // Update camera vectors + front = glm::normalize(targetPosition - desiredPosition); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Updates the camera's yaw and pitch based on user input +2. Constrains the pitch to prevent the camera from flipping +3. Calculates a new camera position using spherical coordinates +4. Keeps the camera focused on the character + +==== Integrating with Character Movement + +To create a complete third-person camera system, we need to integrate it with character movement. Here's an example of how to use the third-person camera in a game loop: + +[source,cpp] +---- +void gameLoop(float deltaTime) { + // Update character position and orientation based on input + character.update(deltaTime); + + // Update camera position to follow the character + thirdPersonCamera.updatePosition( + character.getPosition(), + character.getForward(), + deltaTime + ); + + // Handle camera occlusion + thirdPersonCamera.handleOcclusion(scene); + + // Process camera orbit input (if any) + if (mouseInputDetected) { + thirdPersonCamera.orbit(mouseDeltaX, mouseDeltaY); + } + + // Get the view and projection matrices for rendering + glm::mat4 viewMatrix = thirdPersonCamera.getViewMatrix(); + glm::mat4 projMatrix = thirdPersonCamera.getProjectionMatrix(aspectRatio); + + // Use these matrices for rendering the scene + renderer.render(scene, viewMatrix, projMatrix); +} +---- + +==== Advanced Techniques + +For even more sophisticated third-person cameras, consider these advanced techniques: + +* *Camera Collision*: Implement a collision volume for the camera to prevent it from passing through walls +* *Context-Aware Positioning*: Adjust camera position based on the environment (e.g., zoom out in large open areas, zoom in in tight spaces) +* *Intelligent Framing*: Adjust the camera to keep both the character and important objects in frame +* *Predictive Following*: Anticipate character movement to reduce camera lag +* *Camera Obstruction Transparency*: Make objects that obstruct the view partially transparent +* *Dynamic Field of View*: Adjust the FOV based on movement speed or environmental context + +==== Performance Considerations + +When implementing occlusion avoidance, be mindful of performance: + +* Use simplified collision geometry for raycasting +* Limit the frequency of occlusion checks +* Consider using spatial partitioning structures (e.g., octrees) to accelerate raycasts +* For mobile or performance-constrained platforms, simplify the occlusion algorithm + +In the next section, we'll explore how to use transformation matrices to position objects in our 3D scene. + +link:04_transformation_matrices.adoc[Next: Transformation Matrices] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc new file mode 100644 index 00000000..07c9b3db --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc @@ -0,0 +1,182 @@ +::pp: {plus}{plus} + += Camera & Transformations: Transformation Matrices +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Transformation Matrices + +In this section, we'll dive deeper into the transformation matrices used in 3D graphics and how they're applied in our rendering pipeline. + +=== The Model-View-Projection (MVP) Pipeline + +The transformation of vertices from object space to screen space involves a series of matrix multiplications, commonly known as the MVP pipeline: + +[source,cpp] +---- +// The complete transformation pipeline +glm::mat4 MVP = projectionMatrix * viewMatrix * modelMatrix; +---- + +Let's explore each of these matrices in detail. + +=== Model Matrix + +The model matrix transforms vertices from object space to world space. It positions, rotates, and scales objects in the world. + +[source,cpp] +---- +glm::mat4 createModelMatrix( + const glm::vec3& position, + const glm::vec3& rotation, + const glm::vec3& scale +) { + // Start with identity matrix + glm::mat4 model = glm::mat4(1.0f); + + // Apply transformations in order: scale, rotate, translate + model = glm::translate(model, position); + + // Apply rotations around each axis + model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + + // Apply scaling + model = glm::scale(model, scale); + + return model; +} +---- + +=== View Matrix + +The view matrix transforms vertices from world space to view space (camera space). It represents the position and orientation of the camera. + +[source,cpp] +---- +glm::mat4 createViewMatrix( + const glm::vec3& cameraPosition, + const glm::vec3& cameraTarget, + const glm::vec3& upVector +) { + return glm::lookAt(cameraPosition, cameraTarget, upVector); +} +---- + +The `lookAt` function creates a view matrix that positions the camera at `cameraPosition`, looking at `cameraTarget`, with `upVector` defining the up direction. + +=== Projection Matrix + +The projection matrix transforms vertices from view space to clip space. It defines how 3D coordinates are projected onto the 2D screen. + +==== Perspective Projection + +Perspective projection simulates how objects appear smaller as they get farther away, which is how our eyes naturally perceive the world. + +[source,cpp] +---- +glm::mat4 createPerspectiveMatrix( + float fovY, + float aspectRatio, + float nearPlane, + float farPlane +) { + return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); +} +---- + +Parameters: +* `fovY`: Field of view angle in degrees (vertical) +* `aspectRatio`: Width divided by height of the viewport +* `nearPlane`: Distance to the near clipping plane +* `farPlane`: Distance to the far clipping plane + +==== Orthographic Projection + +Orthographic projection doesn't have perspective distortion, making it useful for 2D rendering or technical drawings. + +[source,cpp] +---- +glm::mat4 createOrthographicMatrix( + float left, + float right, + float bottom, + float top, + float nearPlane, + float farPlane +) { + return glm::ortho(left, right, bottom, top, nearPlane, farPlane); +} +---- + +=== Normal Matrix + +When applying non-uniform scaling to objects, normals can become incorrect if transformed with the model matrix. The normal matrix solves this issue: + +[source,cpp] +---- +glm::mat3 createNormalMatrix(const glm::mat4& modelMatrix) { + // The normal matrix is the transpose of the inverse of the upper-left 3x3 part of the model matrix + return glm::transpose(glm::inverse(glm::mat3(modelMatrix))); +} +---- + +=== Applying Transformations in Shaders + +In Vulkan, we typically pass these matrices to our shaders as uniform variables: + +[source,glsl] +---- +// Vertex shader +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragNormal; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + // Apply MVP transformation + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + + // Transform normal using normal matrix + mat3 normalMatrix = transpose(inverse(mat3(ubo.model))); + fragNormal = normalMatrix * inNormal; + + fragTexCoord = inTexCoord; +} +---- + +=== Hierarchical Transformations + +For complex objects or scenes with parent-child relationships, we use hierarchical transformations: + +[source,cpp] +---- +// Parent transformation +glm::mat4 parentModel = createModelMatrix(parentPosition, parentRotation, parentScale); + +// Child transformation relative to parent +glm::mat4 localModel = createModelMatrix(childLocalPosition, childLocalRotation, childLocalScale); + +// Combined transformation +glm::mat4 childWorldModel = parentModel * localModel; +---- + +In the next section, we'll integrate our camera system and transformation matrices with Vulkan to render 3D scenes. + +link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc new file mode 100644 index 00000000..21bd479d --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -0,0 +1,263 @@ +::pp: {plus}{plus} + += Camera & Transformations: Vulkan Integration +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Integrating Camera with Vulkan + +Now that we have a camera system and understand transformation matrices, let's integrate them with our Vulkan application. We'll focus on how to set up uniform buffers for our matrices and update them each frame based on camera movement. + +=== Uniform Buffer Setup + +First, we need to define our uniform buffer structure: + +[source,cpp] +---- +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; +---- + +Next, we'll create the uniform buffer and its descriptor set: + +[source,cpp] +---- +void createUniformBuffers() { + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + + uniformBuffers.resize(swapChainImages.size()); + uniformBuffersMapped.resize(swapChainImages.size()); + + for (size_t i = 0; i < swapChainImages.size(); i++) { + // Create the buffer + vk::BufferCreateInfo bufferInfo{ + .size = bufferSize, + .usage = vk::BufferUsageFlagBits::eUniformBuffer, + .sharingMode = vk::SharingMode::eExclusive + }; + + uniformBuffers[i] = vk::raii::Buffer(device, bufferInfo); + + // Allocate and bind memory + vk::MemoryRequirements memRequirements = uniformBuffers[i].getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ) + }; + + uniformBuffersMemory[i] = vk::raii::DeviceMemory(device, allocInfo); + uniformBuffers[i].bindMemory(*uniformBuffersMemory[i], 0); + + // Persistently map the buffer memory + uniformBuffersMapped[i] = uniformBuffersMemory[i].mapMemory(0, bufferSize); + } +} +---- + +=== Descriptor Set Layout + +We need to create a descriptor set layout that describes our uniform buffer: + +[source,cpp] +---- +void createDescriptorSetLayout() { + vk::DescriptorSetLayoutBinding uboLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex, + .pImmutableSamplers = nullptr + }; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = 1, + .pBindings = &uboLayoutBinding + }; + + descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo); +} +---- + +=== Descriptor Sets + +Now we'll create descriptor sets that point to our uniform buffers: + +[source,cpp] +---- +void createDescriptorSets() { + std::vector layouts(swapChainImages.size(), *descriptorSetLayout); + + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(swapChainImages.size()), + .pSetLayouts = layouts.data() + }; + + descriptorSets = device.allocateDescriptorSets(allocInfo); + + for (size_t i = 0; i < swapChainImages.size(); i++) { + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }; + + device.updateDescriptorSets(1, &descriptorWrite, 0, nullptr); + } +} +---- + +=== Updating Uniform Buffers + +In our main loop, we'll update the uniform buffer with the latest camera data: + +[source,cpp] +---- +void updateUniformBuffer(uint32_t currentImage) { + static auto startTime = std::chrono::high_resolution_clock::now(); + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); + + UniformBufferObject ubo{}; + + // Model matrix: rotate the model around the Y axis + ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f)); + + // View matrix: get from our camera + ubo.view = camera.getViewMatrix(); + + // Projection matrix: get from our camera + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + + // Vulkan's Y coordinate is inverted compared to OpenGL + ubo.proj[1][1] *= -1; + + // Copy the data to the uniform buffer + memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +} +---- + +=== Handling Input for Camera Movement + +We need to handle user input to control the camera: + +[source,cpp] +---- +void processInput() { + // Calculate delta time + static float lastFrame = 0.0f; + float currentFrame = glfwGetTime(); + float deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; + + // Process keyboard input for camera movement + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); +} +---- + +=== Mouse Callback for Camera Rotation + +We'll also need to handle mouse movement for camera rotation: + +[source,cpp] +---- +// Global variables for mouse handling +float lastX = 0.0f, lastY = 0.0f; +bool firstMouse = true; + +void mouseCallback(GLFWwindow* window, double xpos, double ypos) { + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; // Reversed: y ranges bottom to top + + lastX = xpos; + lastY = ypos; + + camera.processMouseMovement(xoffset, yoffset); +} + +void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + camera.processMouseScroll(yoffset); +} +---- + +=== Setting Up Input Callbacks + +In our initialization code, we need to set up the input callbacks: + +[source,cpp] +---- +void initWindow() { + // ... existing GLFW initialization code ... + + // Set up input callbacks + glfwSetCursorPosCallback(window, mouseCallback); + glfwSetScrollCallback(window, scrollCallback); + + // Capture the cursor for camera control + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} +---- + +=== Main Loop Integration + +Finally, we integrate everything in our main loop: + +[source,cpp] +---- +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + processInput(); + + // Update uniform buffer with latest camera data + updateUniformBuffer(currentFrame); + + // Draw frame + drawFrame(); + } +} +---- + +With these components in place, we now have a fully functional camera system integrated with our Vulkan application. Users can navigate the 3D scene using keyboard and mouse controls, and the view will update accordingly. + +In the next section, we'll wrap up with a conclusion and discuss potential improvements to our camera system. + +link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc new file mode 100644 index 00000000..6b635d64 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc @@ -0,0 +1,62 @@ +::pp: {plus}{plus} + += Camera & Transformations: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've built a comprehensive camera system for our Vulkan application. Let's summarize what we've learned and discuss potential improvements. + +=== What We've Learned + +* *Mathematical Foundations*: We explored the essential mathematical concepts for 3D graphics, including vectors, matrices, quaternions, and coordinate systems. + +* *Camera Implementation*: We designed a flexible camera class that supports different movement modes and handles user input for navigation. + +* *Transformation Matrices*: We examined the model, view, and projection matrices that form the MVP pipeline, and how they transform vertices through different coordinate spaces. + +* *Vulkan Integration*: We integrated our camera system with Vulkan by setting up uniform buffers, descriptor sets, and input handling. + +With these components in place, we now have a solid foundation for creating interactive 3D applications with Vulkan. Our camera system allows users to navigate and explore 3D scenes from any perspective. + +=== Potential Improvements + +While our camera system is functional, there are several ways it could be enhanced: + +* *Camera Modes*: Implement different camera modes (first-person, third-person, orbit) that can be switched at runtime. + +* *Smooth Transitions*: Add interpolation between camera positions and orientations for smoother transitions. + +* *Collision Detection*: Implement collision detection to prevent the camera from passing through objects or walls. + +* *Camera Paths*: Create a system for defining and following predefined camera paths for cinematic sequences. + +* *Camera Effects*: Add support for camera effects like depth of field, motion blur, or screen-space reflections. + +* *Performance Optimization*: Optimize the camera system for performance, especially for mobile or VR applications. + +=== Next Steps + +As you continue building your Vulkan engine, consider how the camera system integrates with other components: + +* *Scene Graph*: How does the camera fit into your scene graph hierarchy? + +* *Rendering Pipeline*: How can you optimize rendering based on the camera's position and orientation? + +* *User Interface*: How will users interact with the camera in your application? + +By addressing these questions, you can create a more cohesive and user-friendly 3D application. + +=== Final Thoughts + +A well-designed camera system is essential for any 3D application. It serves as the user's window into your virtual world and significantly impacts the user experience. By understanding the mathematical foundations and implementing a flexible camera system, you've taken a major step toward creating immersive 3D applications with Vulkan. + +Remember that the code provided in this chapter is a starting point. Feel free to modify and extend it to suit your specific needs and application requirements. + +link:../Engine_Architecture/conclusion.adoc[Previous: Engine Architecture] | link:../Lighting_Materials/01_introduction.adoc[Next: Lighting & Materials] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc new file mode 100644 index 00000000..ecb8b843 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc @@ -0,0 +1,21 @@ +::pp: {plus}{plus} + += Camera & Transformations +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +This chapter covers the implementation of a 3D camera system and the mathematical foundations of 3D transformations in Vulkan. + +== Contents + +* link:01_introduction.adoc[Introduction] +* link:02_math_foundations.adoc[Mathematical Foundations] +* link:03_camera_implementation.adoc[Camera Implementation] +* link:04_transformation_matrices.adoc[Transformation Matrices] +* link:05_vulkan_integration.adoc[Vulkan Integration] +* link:06_conclusion.adoc[Conclusion] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc new file mode 100644 index 00000000..4beb91fb --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -0,0 +1,62 @@ +:pp: {plus}{plus} + += Engine Architecture: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction + +Welcome to the "Engine Architecture" chapter of our "Building a Simple +Engine" series! In this chapter, we'll explore the fundamental architectural +patterns and design principles that form the backbone of a modern Vulkan +rendering engine. + +We'll start by taking a step back and considering the overall structure of our engine. A +well-designed architecture is crucial for creating a flexible, maintainable, and extensible rendering system. + +=== What You'll Learn + +In this chapter, we'll cover: + +* *Architectural Patterns* - Common design patterns used in game and rendering engines, including their strengths and trade-offs. + +* *Component Systems* - How to implement a flexible component-based architecture that allows for modular and reusable code. + +* *Resource Management* - Strategies for efficiently managing and accessing various resources like textures, models, and shaders. + +* *Rendering Pipeline* - Designing a flexible rendering pipeline that can accommodate different rendering techniques and effects. + +* *Event Systems* - Implementing communication between different parts of your engine through event-driven architecture. + +=== Prerequisites + +Before starting this chapter, you should have completed: + +* The main Vulkan tutorial series + +You should also be familiar with: + +* Object-oriented programming concepts +* Basic design patterns (Observer, Factory, Singleton, etc.) +* Modern C++ features (smart pointers, templates, etc.) + +=== Why Architecture Matters + +A well-designed engine architecture provides several benefits: + +1. *Maintainability* - Clean separation of concerns makes it easier to update and fix individual components without affecting the entire system. + +2. *Extensibility* - A modular design allows you to add new features without major refactoring. + +3. *Reusability* - Well-encapsulated components can be reused across different projects or parts of the same project. + +4. *Performance* - A thoughtful architecture can enable optimizations like multithreading, batching, and caching. + +Let's begin our exploration of engine architecture with an overview of common architectural patterns used in modern rendering engines. + +link:02_architectural_patterns.adoc[Next: Architectural Patterns] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc new file mode 100644 index 00000000..b2afa969 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -0,0 +1,325 @@ +:pp: {plus}{plus} + += Engine Architecture: Architectural Patterns +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Architectural Patterns + +In this section, we'll explore common architectural patterns used in modern rendering engines. Understanding these patterns will help you make informed decisions when designing your own engine architecture. + +=== Layered Architecture + +One of the most fundamental architectural patterns is the layered architecture, where the system is divided into distinct layers, each with a specific responsibility. + +==== Typical Layers in a Rendering Engine + +1. *Application Layer* - Handles user input, game logic, and high-level application flow. +2. *Scene Management Layer* - Manages the scene graph, spatial partitioning, and culling. +3. *Rendering Layer* - Handles the rendering pipeline, shaders, and graphics API interaction. +4. *Resource Management Layer* - Manages loading, caching, and unloading of assets. +5. *Platform Abstraction Layer* - Provides a consistent interface to platform-specific functionality. + +==== Benefits of Layered Architecture + +* Clear separation of concerns +* Easier to understand and maintain +* Can replace or modify individual layers without affecting others +* Facilitates testing of individual layers + +==== Implementation Example + +[source,cpp] +---- +// Platform Abstraction Layer +class Platform { +public: + virtual void Initialize() = 0; + virtual void* CreateWindow(int width, int height) = 0; + virtual void ProcessEvents() = 0; + // ... +}; + +// Resource Management Layer +class ResourceManager { +public: + virtual Texture* LoadTexture(const std::string& path) = 0; + virtual Mesh* LoadMesh(const std::string& path) = 0; + // ... +}; + +// Rendering Layer +class Renderer { +public: + virtual void Initialize(Platform* platform) = 0; + virtual void RenderScene(Scene* scene) = 0; + // ... +}; + +// Scene Management Layer +class SceneManager { +public: + virtual void AddEntity(Entity* entity) = 0; + virtual void UpdateScene(float deltaTime) = 0; + // ... +}; + +// Application Layer +class Application { +private: + Platform* platform; + ResourceManager* resourceManager; + Renderer* renderer; + SceneManager* sceneManager; + +public: + void Run() { + platform->Initialize(); + renderer->Initialize(platform); + + // Main loop + while (running) { + platform->ProcessEvents(); + sceneManager->UpdateScene(deltaTime); + renderer->RenderScene(sceneManager->GetActiveScene()); + } + } +}; +---- + +=== Component-Based Architecture + +Component-based architecture is widely used in modern game engines. It promotes composition over inheritance and allows for more flexible entity design. + +==== Key Concepts + +1. *Entities* - Basic containers that represent objects in the game world. +2. *Components* - Modular pieces of functionality that can be attached to entities. +3. *Systems* - Process entities with specific components to implement game logic. + +==== Benefits of Component-Based Architecture + +* Highly modular and flexible +* Avoids deep inheritance hierarchies +* Enables data-oriented design +* Facilitates parallel processing + +==== Implementation Example + +[source,cpp] +---- +// Component base class +class Component { +public: + virtual ~Component() = default; + virtual void Update(float deltaTime) {} +}; + +// Specific component types +class TransformComponent : public Component { +private: + glm::vec3 position; + glm::quat rotation; + glm::vec3 scale; + +public: + // Methods to manipulate transform +}; + +class MeshComponent : public Component { +private: + Mesh* mesh; + Material* material; + +public: + // Methods to render the mesh +}; + +// Entity class +class Entity { +private: + std::vector> components; + +public: + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + for (auto& component : components) { + if (T* result = dynamic_cast(component.get())) { + return result; + } + } + return nullptr; + } + + void Update(float deltaTime) { + for (auto& component : components) { + component->Update(deltaTime); + } + } +}; +---- + +=== Data-Oriented Design + +Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. + +==== Key Concepts + +1. *Data Layout* - Organizing data for cache-friendly access patterns. +2. *Systems* - Process data in bulk, often using SIMD instructions. +3. *Entity-Component-System (ECS)* - A common implementation of DOD principles. + +==== Benefits of Data-Oriented Design + +* Better cache utilization +* More efficient memory usage +* Easier to parallelize +* Can lead to significant performance improvements + +==== Implementation Example + +[source,cpp] +---- +// A simple ECS implementation +struct TransformData { + std::vector positions; + std::vector rotations; + std::vector scales; +}; + +struct RenderData { + std::vector meshes; + std::vector materials; +}; + +class TransformSystem { +private: + TransformData& transformData; + +public: + TransformSystem(TransformData& data) : transformData(data) {} + + void Update(float deltaTime) { + // Process all transforms in bulk + for (size_t i = 0; i < transformData.positions.size(); ++i) { + // Update transforms + } + } +}; + +class RenderSystem { +private: + RenderData& renderData; + TransformData& transformData; + +public: + RenderSystem(RenderData& rData, TransformData& tData) + : renderData(rData), transformData(tData) {} + + void Render() { + // Render all entities in bulk + for (size_t i = 0; i < renderData.meshes.size(); ++i) { + // Render mesh with transform + } + } +}; +---- + +=== Service Locator Pattern + +The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. + +==== Key Concepts + +1. *Service Interface* - Defines the contract for a service. +2. *Service Provider* - Implements the service interface. +3. *Service Locator* - Provides access to services. + +==== Benefits of Service Locator Pattern + +* Decouples service consumers from service providers +* Allows for easy service replacement +* Facilitates testing with mock services + +==== Implementation Example + +[source,cpp] +---- +// Audio service interface +class IAudioService { +public: + virtual ~IAudioService() = default; + virtual void PlaySound(const std::string& soundName) = 0; + virtual void StopSound(const std::string& soundName) = 0; +}; + +// Concrete audio service +class OpenALAudioService : public IAudioService { +public: + void PlaySound(const std::string& soundName) override { + // Implementation using OpenAL + } + + void StopSound(const std::string& soundName) override { + // Implementation using OpenAL + } +}; + +// Service locator +class ServiceLocator { +private: + static IAudioService* audioService; + static IAudioService nullAudioService; // Default null service + +public: + static void Initialize() { + audioService = &nullAudioService; + } + + static IAudioService& GetAudioService() { + return *audioService; + } + + static void ProvideAudioService(IAudioService* service) { + if (service == nullptr) { + audioService = &nullAudioService; + } else { + audioService = service; + } + } +}; + +// Usage example +void PlayGameSound() { + ServiceLocator::GetAudioService().PlaySound("explosion"); +} +---- + +=== Conclusion + +These architectural patterns provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. + +When designing your engine architecture, consider: + +1. *Performance Requirements* - Different patterns have different performance characteristics. +2. *Flexibility Needs* - How much flexibility do you need for future extensions? +3. *Team Size and Experience* - More complex architectures may be harder to work with for smaller teams. +4. *Project Scope* - A small project may not need the complexity of a full ECS. + +In the next section, we'll dive deeper into component systems and how to implement them effectively in your engine. + +link:01_introduction.adoc[Previous: Introduction] | link:03_component_systems.adoc[Next: Component Systems] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc new file mode 100644 index 00000000..9735b535 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc @@ -0,0 +1,554 @@ +:pp: {plus}{plus} + += Engine Architecture: Component Systems +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Component Systems + +In the previous section, we introduced the concept of component-based architecture. Now, let's dive deeper into how to implement effective component systems in your rendering engine. + +=== The Problem with Deep Inheritance + +Traditional game object systems often rely on deep inheritance hierarchies: + +[source,cpp] +---- +class GameObject { /* ... */ }; +class PhysicalObject : public GameObject { /* ... */ }; +class Character : public PhysicalObject { /* ... */ }; +class Player : public Character { /* ... */ }; +class Enemy : public Character { /* ... */ }; +class FlyingEnemy : public Enemy { /* ... */ }; +// And so on... +---- + +This approach has several drawbacks: + +1. *Rigidity* - Adding new combinations of behaviors requires creating new classes. +2. *Code Duplication* - Similar functionality may be duplicated across different branches of the hierarchy. +3. *Bloated Classes* - Base classes tend to accumulate functionality over time. +4. *Difficult Refactoring* - Changes to base classes can have far-reaching consequences. + +=== Component-Based Design Principles + +Component-based design addresses these issues by favoring composition over inheritance: + +1. *Single Responsibility* - Each component should have a single, well-defined responsibility. +2. *Encapsulation* - Components should encapsulate their internal state and behavior. +3. *Loose Coupling* - Components should minimize dependencies on other components. +4. *Reusability* - Components should be designed for reuse across different entity types. + +=== Basic Component System Implementation + +Let's build a more complete component system based on the example from the previous section: + +[source,cpp] +---- +// Forward declarations +class Entity; + +// Base component class +class Component { +protected: + Entity* owner = nullptr; + +public: + virtual ~Component() = default; + + virtual void Initialize() {} + virtual void Update(float deltaTime) {} + virtual void Render() {} + + void SetOwner(Entity* entity) { owner = entity; } + Entity* GetOwner() const { return owner; } +}; + +// Entity class +class Entity { +private: + std::string name; + bool active = true; + std::vector> components; + +public: + explicit Entity(const std::string& entityName) : name(entityName) {} + + const std::string& GetName() const { return name; } + bool IsActive() const { return active; } + void SetActive(bool isActive) { active = isActive; } + + void Initialize() { + for (auto& component : components) { + component->Initialize(); + } + } + + void Update(float deltaTime) { + if (!active) return; + + for (auto& component : components) { + component->Update(deltaTime); + } + } + + void Render() { + if (!active) return; + + for (auto& component : components) { + component->Render(); + } + } + + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + + // Check if component of this type already exists + for (auto& component : components) { + if (dynamic_cast(component.get())) { + return dynamic_cast(component.get()); + } + } + + // Create new component + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + componentPtr->SetOwner(this); + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + for (auto& component : components) { + if (T* result = dynamic_cast(component.get())) { + return result; + } + } + return nullptr; + } + + template + bool RemoveComponent() { + for (auto it = components.begin(); it != components.end(); ++it) { + if (dynamic_cast(it->get())) { + components.erase(it); + return true; + } + } + return false; + } +}; +---- + +=== Common Component Types + +Let's implement some common component types that you might use in a rendering engine: + +[source,cpp] +---- +// Transform component +class TransformComponent : public Component { +private: + glm::vec3 position = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + glm::vec3 scale = glm::vec3(1.0f); + + // Cached transformation matrix + mutable glm::mat4 transformMatrix = glm::mat4(1.0f); + mutable bool transformDirty = true; + +public: + void SetPosition(const glm::vec3& pos) { + position = pos; + transformDirty = true; + } + + void SetRotation(const glm::quat& rot) { + rotation = rot; + transformDirty = true; + } + + void SetScale(const glm::vec3& s) { + scale = s; + transformDirty = true; + } + + const glm::vec3& GetPosition() const { return position; } + const glm::quat& GetRotation() const { return rotation; } + const glm::vec3& GetScale() const { return scale; } + + glm::mat4 GetTransformMatrix() const { + if (transformDirty) { + // Calculate transformation matrix + glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), position); + glm::mat4 rotationMatrix = glm::mat4_cast(rotation); + glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale); + + transformMatrix = translationMatrix * rotationMatrix * scaleMatrix; + transformDirty = false; + } + return transformMatrix; + } +}; + +// Mesh component +class MeshComponent : public Component { +private: + Mesh* mesh = nullptr; + Material* material = nullptr; + +public: + MeshComponent(Mesh* m, Material* mat) : mesh(m), material(mat) {} + + void SetMesh(Mesh* m) { mesh = m; } + void SetMaterial(Material* mat) { material = mat; } + + Mesh* GetMesh() const { return mesh; } + Material* GetMaterial() const { return material; } + + void Render() override { + if (!mesh || !material) return; + + // Get transform component + auto transform = GetOwner()->GetComponent(); + if (!transform) return; + + // Render mesh with material and transform + material->Bind(); + material->SetUniform("modelMatrix", transform->GetTransformMatrix()); + mesh->Render(); + } +}; + +// Camera component +class CameraComponent : public Component { +private: + float fieldOfView = 45.0f; + float aspectRatio = 16.0f / 9.0f; + float nearPlane = 0.1f; + float farPlane = 1000.0f; + + glm::mat4 viewMatrix = glm::mat4(1.0f); + glm::mat4 projectionMatrix = glm::mat4(1.0f); + bool projectionDirty = true; + +public: + void SetPerspective(float fov, float aspect, float near, float far) { + fieldOfView = fov; + aspectRatio = aspect; + nearPlane = near; + farPlane = far; + projectionDirty = true; + } + + glm::mat4 GetViewMatrix() const { + // Get transform component + auto transform = GetOwner()->GetComponent(); + if (transform) { + // Calculate view matrix from transform + glm::vec3 position = transform->GetPosition(); + glm::quat rotation = transform->GetRotation(); + + // Forward vector (local -Z) + glm::vec3 forward = rotation * glm::vec3(0.0f, 0.0f, -1.0f); + // Up vector (local +Y) + glm::vec3 up = rotation * glm::vec3(0.0f, 1.0f, 0.0f); + + return glm::lookAt(position, position + forward, up); + } + return glm::mat4(1.0f); + } + + glm::mat4 GetProjectionMatrix() const { + if (projectionDirty) { + projectionMatrix = glm::perspective( + glm::radians(fieldOfView), + aspectRatio, + nearPlane, + farPlane + ); + projectionDirty = false; + } + return projectionMatrix; + } +}; +---- + +=== Component Communication + +Components often need to communicate with each other. There are several approaches to component communication: + +==== Direct References + +The simplest approach is to use direct references: + +[source,cpp] +---- +void MeshComponent::Update(float deltaTime) { + auto transform = GetOwner()->GetComponent(); + if (transform) { + // Use transform data + } +} +---- + +This approach is straightforward but creates tight coupling between +components. Tight coupling makes it very difficult to impossible to create +unit tests and properly test the engine, so this approach should be avoided +in production code. + +==== Event System + +A more flexible approach is to use an event system: + +[source,cpp] +---- +// Event base class +class Event { +public: + virtual ~Event() = default; +}; + +// Specific event types +class CollisionEvent : public Event { +private: + Entity* entity1; + Entity* entity2; + +public: + CollisionEvent(Entity* e1, Entity* e2) : entity1(e1), entity2(e2) {} + + Entity* GetEntity1() const { return entity1; } + Entity* GetEntity2() const { return entity2; } +}; + +// Event listener interface +class EventListener { +public: + virtual ~EventListener() = default; + virtual void OnEvent(const Event& event) = 0; +}; + +// Event system +class EventSystem { +private: + std::vector listeners; + +public: + void AddListener(EventListener* listener) { + listeners.push_back(listener); + } + + void RemoveListener(EventListener* listener) { + auto it = std::find(listeners.begin(), listeners.end(), listener); + if (it != listeners.end()) { + listeners.erase(it); + } + } + + void DispatchEvent(const Event& event) { + for (auto listener : listeners) { + listener->OnEvent(event); + } + } +}; + +// Component that listens for events +class PhysicsComponent : public Component, public EventListener { +public: + void Initialize() override { + // Register as event listener + GetEventSystem().AddListener(this); + } + + ~PhysicsComponent() override { + // Unregister as event listener + GetEventSystem().RemoveListener(this); + } + + void OnEvent(const Event& event) override { + if (auto collisionEvent = dynamic_cast(&event)) { + // Handle collision event + } + } + +private: + EventSystem& GetEventSystem() { + // Get event system from somewhere (e.g., service locator) + static EventSystem eventSystem; + return eventSystem; + } +}; +---- + +This approach decouples components but adds complexity. Crucially, a +decoupled component is a component that can be tested independently of any +other component. + +=== Component Lifecycle Management + +Managing the lifecycle of components is crucial for a robust component system: + +[source,cpp] +---- +class Component { +public: + enum class State { + Uninitialized, + Initializing, + Active, + Destroying, + Destroyed + }; + +private: + State state = State::Uninitialized; + Entity* owner = nullptr; + +public: + virtual ~Component() { + if (state != State::Destroyed) { + OnDestroy(); + state = State::Destroyed; + } + } + + void Initialize() { + if (state == State::Uninitialized) { + state = State::Initializing; + OnInitialize(); + state = State::Active; + } + } + + void Destroy() { + if (state == State::Active) { + state = State::Destroying; + OnDestroy(); + state = State::Destroyed; + } + } + + bool IsActive() const { return state == State::Active; } + + void SetOwner(Entity* entity) { owner = entity; } + Entity* GetOwner() const { return owner; } + +protected: + virtual void OnInitialize() {} + virtual void OnDestroy() {} + virtual void Update(float deltaTime) {} + virtual void Render() {} + + friend class Entity; // Allow Entity to call protected methods +}; +---- + +=== Optimizing Component Access + +The `GetComponent()` method shown earlier uses dynamic_cast, which can be slow. Here's an optimized approach using component type IDs: + +[source,cpp] +---- +// Component type ID system +class ComponentTypeIDSystem { +private: + static size_t nextTypeID; + +public: + template + static size_t GetTypeID() { + static size_t typeID = nextTypeID++; + return typeID; + } +}; + +size_t ComponentTypeIDSystem::nextTypeID = 0; + +// Component base class with type ID +class Component { +public: + virtual ~Component() = default; + + template + static size_t GetTypeID() { + return ComponentTypeIDSystem::GetTypeID(); + } +}; + +// Entity with optimized component access +class Entity { +private: + std::vector> components; + std::unordered_map componentMap; + +public: + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + + size_t typeID = Component::GetTypeID(); + + // Check if component of this type already exists + auto it = componentMap.find(typeID); + if (it != componentMap.end()) { + return static_cast(it->second); + } + + // Create new component + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + componentMap[typeID] = componentPtr; + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + size_t typeID = Component::GetTypeID(); + auto it = componentMap.find(typeID); + if (it != componentMap.end()) { + return static_cast(it->second); + } + return nullptr; + } + + template + bool RemoveComponent() { + size_t typeID = Component::GetTypeID(); + auto it = componentMap.find(typeID); + if (it != componentMap.end()) { + Component* componentPtr = it->second; + componentMap.erase(it); + + for (auto compIt = components.begin(); compIt != components.end(); ++compIt) { + if (compIt->get() == componentPtr) { + components.erase(compIt); + return true; + } + } + } + return false; + } +}; +---- + +=== Conclusion + +Component systems provide a flexible and modular approach to building game objects in your engine. By following the principles outlined in this section, you can create a robust component system that: + +1. Promotes code reuse through composition +2. Reduces coupling between different parts of your engine +3. Allows for flexible entity creation without deep inheritance hierarchies +4. Can be optimized for performance + +In the next section, we'll explore resource management systems, which are crucial for efficiently handling assets in your engine. + +link:02_architectural_patterns.adoc[Previous: Architectural Patterns] | link:04_resource_management.adoc[Next: Resource Management] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc new file mode 100644 index 00000000..6ac3a5c8 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc @@ -0,0 +1,685 @@ +:pp: {plus}{plus} + += Engine Architecture: Resource Management +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Resource Management + +Efficient resource management is a critical aspect of any rendering engine. In this section, we'll explore strategies for managing various types of resources, such as textures, meshes, shaders, and materials. + +=== Resource Management Challenges + +When designing a resource management system, you'll need to address several challenges: + +1. *Loading and Unloading* - Resources need to be loaded from disk and unloaded when no longer needed. +2. *Caching* - Frequently used resources should be cached to avoid redundant loading. +3. *Reference Counting* - Track how many objects are using a resource to know when it can be safely unloaded. +4. *Hot Reloading* - Allow resources to be updated while the application is running (useful during development). +5. *Streaming* - Load resources asynchronously to avoid blocking the main thread. +6. *Memory Management* - Efficiently allocate and deallocate memory for resources. + +=== Resource Handles + +Instead of directly exposing resource pointers, it's often better to use resource handles: + +[source,cpp] +---- +// Resource handle +template +class ResourceHandle { +private: + std::string resourceId; + ResourceManager* resourceManager; + +public: + ResourceHandle() : resourceManager(nullptr) {} + + ResourceHandle(const std::string& id, ResourceManager* manager) + : resourceId(id), resourceManager(manager) {} + + T* Get() const { + if (!resourceManager) return nullptr; + return resourceManager->GetResource(resourceId); + } + + bool IsValid() const { + return resourceManager && resourceManager->HasResource(resourceId); + } + + const std::string& GetId() const { + return resourceId; + } + + // Convenience operators + T* operator->() const { + return Get(); + } + + T& operator*() const { + return *Get(); + } + + operator bool() const { + return IsValid(); + } +}; +---- + +Using handles instead of direct pointers provides several benefits: + +1. *Indirection* - The resource manager can move resources in memory without invalidating references. +2. *Validation* - Handles can be checked for validity before use. +3. *Automatic Resource Management* - The resource manager can track which resources are in use. + +=== Basic Resource Manager + +Let's implement a basic resource manager that can handle different types of resources: + +[source,cpp] +---- +// Resource base class +class Resource { +private: + std::string resourceId; + bool loaded = false; + +public: + explicit Resource(const std::string& id) : resourceId(id) {} + virtual ~Resource() = default; + + const std::string& GetId() const { return resourceId; } + bool IsLoaded() const { return loaded; } + + virtual bool Load() { + loaded = true; + return true; + } + + virtual void Unload() { + loaded = false; + } +}; + +// Resource manager +class ResourceManager { +private: + // Store resources by type and ID + std::unordered_map>> resources; + + // Reference counts for resources + std::unordered_map refCounts; + +public: + template + ResourceHandle Load(const std::string& resourceId) { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + // Check if resource already exists + auto& typeResources = resources[std::type_index(typeid(T))]; + auto it = typeResources.find(resourceId); + + if (it != typeResources.end()) { + // Resource exists, increment reference count + refCounts[resourceId]++; + return ResourceHandle(resourceId, this); + } + + // Create and load new resource + auto resource = std::make_shared(resourceId); + if (!resource->Load()) { + // Failed to load + return ResourceHandle(); + } + + // Store resource and set reference count + typeResources[resourceId] = resource; + refCounts[resourceId] = 1; + + return ResourceHandle(resourceId, this); + } + + template + T* GetResource(const std::string& resourceId) { + auto& typeResources = resources[std::type_index(typeid(T))]; + auto it = typeResources.find(resourceId); + + if (it != typeResources.end()) { + return static_cast(it->second.get()); + } + + return nullptr; + } + + template + bool HasResource(const std::string& resourceId) { + auto& typeResources = resources[std::type_index(typeid(T))]; + return typeResources.find(resourceId) != typeResources.end(); + } + + void Release(const std::string& resourceId) { + auto it = refCounts.find(resourceId); + if (it != refCounts.end()) { + it->second--; + + if (it->second <= 0) { + // No more references, unload the resource + for (auto& [type, typeResources] : resources) { + auto resourceIt = typeResources.find(resourceId); + if (resourceIt != typeResources.end()) { + resourceIt->second->Unload(); + typeResources.erase(resourceIt); + break; + } + } + + refCounts.erase(it); + } + } + } + + void UnloadAll() { + for (auto& [type, typeResources] : resources) { + for (auto& [id, resource] : typeResources) { + resource->Unload(); + } + typeResources.clear(); + } + refCounts.clear(); + } +}; +---- + +=== Implementing Specific Resource Types + +Now let's implement some specific resource types: + +[source,cpp] +---- +// Texture resource +class Texture : public Resource { +private: + vk::Image image; + vk::DeviceMemory memory; + vk::ImageView imageView; + vk::Sampler sampler; + + int width = 0; + int height = 0; + int channels = 0; + +public: + explicit Texture(const std::string& id) : Resource(id) {} + + ~Texture() override { + Unload(); + } + + bool Load() override { + // Load texture from file + std::string filePath = "textures/" + GetId() + ".ktx"; + + // Load image data using a library like stb_image or ktx + unsigned char* data = LoadImageData(filePath, &width, &height, &channels); + if (!data) { + return false; + } + + // Create Vulkan image, allocate memory, and upload data + CreateVulkanImage(data, width, height, channels); + + // Free image data + FreeImageData(data); + + return Resource::Load(); + } + + void Unload() override { + // Destroy Vulkan resources + if (IsLoaded()) { + // Get device from somewhere (e.g., singleton or parameter) + vk::Device device = GetDevice(); + + device.destroySampler(sampler); + device.destroyImageView(imageView); + device.destroyImage(image); + device.freeMemory(memory); + + Resource::Unload(); + } + } + + // Getters for Vulkan resources + vk::Image GetImage() const { return image; } + vk::ImageView GetImageView() const { return imageView; } + vk::Sampler GetSampler() const { return sampler; } + +private: + unsigned char* LoadImageData(const std::string& filePath, int* width, int* height, int* channels) { + // Implementation using stb_image or ktx library + // ... + return nullptr; // Placeholder + } + + void FreeImageData(unsigned char* data) { + // Implementation using stb_image or ktx library + // ... + } + + void CreateVulkanImage(unsigned char* data, int width, int height, int channels) { + // Implementation to create Vulkan image, allocate memory, and upload data + // ... + } + + vk::Device GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // ... + return vk::Device(); // Placeholder + } +}; + +// Mesh resource +class Mesh : public Resource { +private: + vk::Buffer vertexBuffer; + vk::DeviceMemory vertexBufferMemory; + uint32_t vertexCount = 0; + + vk::Buffer indexBuffer; + vk::DeviceMemory indexBufferMemory; + uint32_t indexCount = 0; + +public: + explicit Mesh(const std::string& id) : Resource(id) {} + + ~Mesh() override { + Unload(); + } + + bool Load() override { + // Load mesh from file + std::string filePath = "models/" + GetId() + ".gltf"; + + // Load mesh data using a library like tinygltf + std::vector vertices; + std::vector indices; + if (!LoadMeshData(filePath, vertices, indices)) { + return false; + } + + // Create Vulkan buffers and upload data + CreateVertexBuffer(vertices); + CreateIndexBuffer(indices); + + vertexCount = static_cast(vertices.size()); + indexCount = static_cast(indices.size()); + + return Resource::Load(); + } + + void Unload() override { + // Destroy Vulkan resources + if (IsLoaded()) { + // Get device from somewhere (e.g., singleton or parameter) + vk::Device device = GetDevice(); + + device.destroyBuffer(indexBuffer); + device.freeMemory(indexBufferMemory); + + device.destroyBuffer(vertexBuffer); + device.freeMemory(vertexBufferMemory); + + Resource::Unload(); + } + } + + // Getters for Vulkan resources + vk::Buffer GetVertexBuffer() const { return vertexBuffer; } + vk::Buffer GetIndexBuffer() const { return indexBuffer; } + uint32_t GetVertexCount() const { return vertexCount; } + uint32_t GetIndexCount() const { return indexCount; } + +private: + bool LoadMeshData(const std::string& filePath, std::vector& vertices, std::vector& indices) { + // Implementation using tinygltf or similar library + // ... + return true; // Placeholder + } + + void CreateVertexBuffer(const std::vector& vertices) { + // Implementation to create Vulkan buffer, allocate memory, and upload data + // ... + } + + void CreateIndexBuffer(const std::vector& indices) { + // Implementation to create Vulkan buffer, allocate memory, and upload data + // ... + } + + vk::Device GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // ... + return vk::Device(); // Placeholder + } +}; + +// Shader resource +class Shader : public Resource { +private: + vk::ShaderModule shaderModule; + vk::ShaderStageFlagBits stage; + +public: + Shader(const std::string& id, vk::ShaderStageFlagBits shaderStage) + : Resource(id), stage(shaderStage) {} + + ~Shader() override { + Unload(); + } + + bool Load() override { + // Determine file extension based on shader stage + std::string extension; + switch (stage) { + case vk::ShaderStageFlagBits::eVertex: extension = ".vert"; break; + case vk::ShaderStageFlagBits::eFragment: extension = ".frag"; break; + case vk::ShaderStageFlagBits::eCompute: extension = ".comp"; break; + default: return false; + } + + // Load shader from file + std::string filePath = "shaders/" + GetId() + extension + ".spv"; + + // Read shader code + std::vector shaderCode; + if (!ReadFile(filePath, shaderCode)) { + return false; + } + + // Create shader module + CreateShaderModule(shaderCode); + + return Resource::Load(); + } + + void Unload() override { + // Destroy Vulkan resources + if (IsLoaded()) { + // Get device from somewhere (e.g., singleton or parameter) + vk::Device device = GetDevice(); + + device.destroyShaderModule(shaderModule); + + Resource::Unload(); + } + } + + // Getters for Vulkan resources + vk::ShaderModule GetShaderModule() const { return shaderModule; } + vk::ShaderStageFlagBits GetStage() const { return stage; } + +private: + bool ReadFile(const std::string& filePath, std::vector& buffer) { + // Implementation to read binary file + // ... + return true; // Placeholder + } + + void CreateShaderModule(const std::vector& code) { + // Implementation to create Vulkan shader module + // ... + } + + vk::Device GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // ... + return vk::Device(); // Placeholder + } +}; +---- + +=== Using the Resource Manager + +Here's how you might use the resource manager in your application: + +[source,cpp] +---- +// Create resource manager +ResourceManager resourceManager; + +// Load resources +auto texture = resourceManager.Load("brick"); +auto mesh = resourceManager.Load("cube"); +auto vertexShader = resourceManager.Load("basic", vk::ShaderStageFlagBits::eVertex); +auto fragmentShader = resourceManager.Load("basic", vk::ShaderStageFlagBits::eFragment); + +// Use resources +if (texture && mesh && vertexShader && fragmentShader) { + // Create material using shaders + Material material(vertexShader, fragmentShader); + + // Set texture in material + material.SetTexture("diffuse", texture); + + // Create entity with mesh and material + Entity entity("MyEntity"); + auto meshComponent = entity.AddComponent(mesh.Get(), &material); +} + +// Resources will be automatically released when handles go out of scope +// or you can explicitly release them +resourceManager.Release(texture.GetId()); +---- + +=== Advanced Resource Management Techniques + +==== Asynchronous Loading + +For large resources, it's often beneficial to load them asynchronously to avoid blocking the main thread: + +[source,cpp] +---- +class AsyncResourceManager { +private: + ResourceManager resourceManager; + std::thread workerThread; + std::queue> taskQueue; + std::mutex queueMutex; + std::condition_variable condition; + bool running = false; + +public: + AsyncResourceManager() { + Start(); + } + + ~AsyncResourceManager() { + Stop(); + } + + void Start() { + running = true; + workerThread = std::thread([this]() { + WorkerThread(); + }); + } + + void Stop() { + { + std::lock_guard lock(queueMutex); + running = false; + } + condition.notify_one(); + if (workerThread.joinable()) { + workerThread.join(); + } + } + + template + void LoadAsync(const std::string& resourceId, std::function)> callback) { + std::lock_guard lock(queueMutex); + taskQueue.push([this, resourceId, callback]() { + auto handle = resourceManager.Load(resourceId); + callback(handle); + }); + condition.notify_one(); + } + +private: + void WorkerThread() { + while (running) { + std::function task; + { + std::unique_lock lock(queueMutex); + condition.wait(lock, [this]() { + return !taskQueue.empty() || !running; + }); + + if (!running && taskQueue.empty()) { + return; + } + + task = std::move(taskQueue.front()); + taskQueue.pop(); + } + + task(); + } + } +}; + +// Usage example +AsyncResourceManager asyncResourceManager; + +asyncResourceManager.LoadAsync("large_texture", [](ResourceHandle texture) { + // This callback will be called when the texture is loaded + if (texture) { + std::cout << "Texture loaded successfully!" << std::endl; + } else { + std::cout << "Failed to load texture." << std::endl; + } +}); +---- + +==== Resource Streaming + +For very large resources like high-resolution textures or detailed meshes, you might want to implement streaming: + +1. *Level of Detail (LOD)* - Load lower-resolution versions first, then progressively load higher-resolution versions. +2. *Texture Streaming* - Load mipmap levels progressively, starting with the smallest. +3. *Mesh Streaming* - Load simplified versions of meshes first, then add detail. + +==== Hot Reloading + +During development, it's useful to be able to update resources without restarting the application: + +[source,cpp] +---- +class HotReloadResourceManager : public ResourceManager { +private: + std::unordered_map fileTimestamps; + std::thread watcherThread; + bool running = false; + +public: + HotReloadResourceManager() { + StartWatcher(); + } + + ~HotReloadResourceManager() { + StopWatcher(); + } + + void StartWatcher() { + running = true; + watcherThread = std::thread([this]() { + WatcherThread(); + }); + } + + void StopWatcher() { + running = false; + if (watcherThread.joinable()) { + watcherThread.join(); + } + } + + template + ResourceHandle Load(const std::string& resourceId) { + auto handle = ResourceManager::Load(resourceId); + + // Store file timestamp + std::string filePath = GetFilePath(resourceId); + try { + fileTimestamps[filePath] = std::filesystem::last_write_time(filePath); + } catch (const std::filesystem::filesystem_error& e) { + // File doesn't exist or can't be accessed + } + + return handle; + } + +private: + template + std::string GetFilePath(const std::string& resourceId) { + // Determine file path based on resource type and ID + if constexpr (std::is_same_v) { + return "textures/" + resourceId + ".ktx"; + } else if constexpr (std::is_same_v) { + return "models/" + resourceId + ".gltf"; + } else if constexpr (std::is_same_v) { + // Simplified for example + return "shaders/" + resourceId + ".spv"; + } else { + return ""; + } + } + + void WatcherThread() { + while (running) { + // Check for file changes + for (auto& [filePath, timestamp] : fileTimestamps) { + try { + auto currentTimestamp = std::filesystem::last_write_time(filePath); + if (currentTimestamp != timestamp) { + // File has changed, reload resource + ReloadResource(filePath); + timestamp = currentTimestamp; + } + } catch (const std::filesystem::filesystem_error& e) { + // File doesn't exist or can't be accessed + } + } + + // Sleep to avoid high CPU usage + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + + void ReloadResource(const std::string& filePath) { + // Extract resource ID and type from file path + // Reload the resource + // ... + } +}; +---- + +=== Conclusion + +A well-designed resource management system is crucial for efficiently handling assets in your rendering engine. By implementing the techniques described in this section, you can create a system that: + +1. Efficiently loads and unloads resources +2. Prevents redundant loading through caching +3. Manages memory usage through reference counting +4. Supports asynchronous loading for better performance +5. Enables hot reloading for faster development + +In the next section, we'll explore rendering pipeline design, which will build upon the resource management system to create a flexible and efficient rendering system. + +link:03_component_systems.adoc[Previous: Component Systems] | link:05_rendering_pipeline.adoc[Next: Rendering Pipeline] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc new file mode 100644 index 00000000..1ca911b4 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -0,0 +1,695 @@ +:pp: {plus}{plus} + += Engine Architecture: Rendering Pipeline +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Rendering Pipeline + +A well-designed rendering pipeline is essential for creating a flexible and efficient rendering engine. In this section, we'll explore how to structure your rendering pipeline to support various rendering techniques and effects. + +=== Rendering Pipeline Challenges + +When designing a rendering pipeline, you'll need to address several challenges: + +1. *Flexibility* - Support different rendering techniques and effects. +2. *Performance* - Efficiently utilize the GPU and minimize state changes. +3. *Extensibility* - Allow for easy addition of new rendering features. +4. *Maintainability* - Keep the code organized and easy to understand. +5. *Platform Independence* - Abstract away platform-specific details. + +=== Rendering Pipeline Architecture + +A modern rendering pipeline typically consists of several stages: + +1. *Scene Culling* - Determine which objects are visible and need to be rendered. +2. *Render Pass Management* - Organize rendering into passes with specific purposes. +3. *Command Generation* - Generate commands for the GPU to execute. +4. *Execution* - Submit commands to the GPU for execution. +5. *Post-Processing* - Apply effects to the rendered image. + +Let's explore each of these stages in detail. + +=== Scene Culling + +Before rendering, we need to determine which objects are visible to the camera. This process is called culling and can significantly improve performance by reducing the number of objects that need to be rendered. + +[source,cpp] +---- +class CullingSystem { +private: + Camera* camera; + std::vector visibleEntities; + +public: + explicit CullingSystem(Camera* cam) : camera(cam) {} + + void SetCamera(Camera* cam) { + camera = cam; + } + + void CullScene(const std::vector& allEntities) { + visibleEntities.clear(); + + if (!camera) return; + + // Get camera frustum + Frustum frustum = camera->GetFrustum(); + + // Check each entity against the frustum + for (auto entity : allEntities) { + if (!entity->IsActive()) continue; + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) continue; + + auto transformComponent = entity->GetComponent(); + if (!transformComponent) continue; + + // Get bounding box of the mesh + BoundingBox boundingBox = meshComponent->GetBoundingBox(); + + // Transform bounding box by entity transform + boundingBox.Transform(transformComponent->GetTransformMatrix()); + + // Check if bounding box is visible + if (frustum.Intersects(boundingBox)) { + visibleEntities.push_back(entity); + } + } + } + + const std::vector& GetVisibleEntities() const { + return visibleEntities; + } +}; +---- + +=== Render Pass Management + +Modern rendering techniques often require multiple passes, each with a specific purpose. A render pass manager helps organize these passes and their dependencies. + +In this tutorial, we use Vulkan's dynamic rendering feature with vk::raii instead of traditional render passes. Dynamic rendering simplifies the rendering process by allowing us to begin and end rendering operations with a single command, without explicitly creating VkRenderPass and VkFramebuffer objects. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Additionally, our engine uses C++20 modules for better code organization, faster compilation times, and improved encapsulation. + +==== Benefits of Dynamic Rendering + +Dynamic rendering offers several advantages over traditional render passes: + +1. *Simplified API*: No need to create and manage VkRenderPass and VkFramebuffer objects, reducing code complexity. +2. *More Flexible Rendering*: Easier to change render targets and attachment formats at runtime. +3. *Improved Compatibility*: Works better with modern rendering techniques that don't fit well into the traditional render pass model. +4. *Reduced State Management*: Fewer objects to track and synchronize. +5. *Easier Debugging*: Simpler rendering code is easier to debug and maintain. + +With dynamic rendering, we specify all rendering state (render targets, load/store operations, etc.) directly in the vkCmdBeginRendering call, rather than setting it up ahead of time in a VkRenderPass object. This allows for more dynamic rendering workflows and simplifies the implementation of techniques like deferred rendering. + +[source,cpp] +---- +// Forward declarations +class RenderPass; +class RenderTarget; + +// Render pass manager +class RenderPassManager { +private: + std::unordered_map> renderPasses; + std::vector sortedPasses; + bool dirty = true; + +public: + template + T* AddRenderPass(const std::string& name, Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from RenderPass"); + + auto it = renderPasses.find(name); + if (it != renderPasses.end()) { + return dynamic_cast(it->second.get()); + } + + auto pass = std::make_unique(std::forward(args)...); + T* passPtr = pass.get(); + renderPasses[name] = std::move(pass); + dirty = true; + + return passPtr; + } + + RenderPass* GetRenderPass(const std::string& name) { + auto it = renderPasses.find(name); + if (it != renderPasses.end()) { + return it->second.get(); + } + return nullptr; + } + + void RemoveRenderPass(const std::string& name) { + auto it = renderPasses.find(name); + if (it != renderPasses.end()) { + renderPasses.erase(it); + dirty = true; + } + } + + void Execute(vk::raii::CommandBuffer& commandBuffer) { + if (dirty) { + SortPasses(); + dirty = false; + } + + for (auto pass : sortedPasses) { + pass->Execute(commandBuffer); + } + } + +private: + void SortPasses() { + // Topologically sort render passes based on dependencies + sortedPasses.clear(); + + // Create a copy of render passes for sorting + std::unordered_map passMap; + for (const auto& [name, pass] : renderPasses) { + passMap[name] = pass.get(); + } + + // Perform topological sort + std::unordered_set visited; + std::unordered_set visiting; + + for (const auto& [name, pass] : passMap) { + if (visited.find(name) == visited.end()) { + TopologicalSort(name, passMap, visited, visiting); + } + } + } + + void TopologicalSort(const std::string& name, + const std::unordered_map& passMap, + std::unordered_set& visited, + std::unordered_set& visiting) { + visiting.insert(name); + + auto pass = passMap.at(name); + for (const auto& dep : pass->GetDependencies()) { + if (visited.find(dep) == visited.end()) { + if (visiting.find(dep) != visiting.end()) { + // Circular dependency detected + throw std::runtime_error("Circular dependency detected in render passes"); + } + TopologicalSort(dep, passMap, visited, visiting); + } + } + + visiting.erase(name); + visited.insert(name); + sortedPasses.push_back(pass); + } +}; + +// Base render pass class +class RenderPass { +private: + std::string name; + std::vector dependencies; + RenderTarget* target = nullptr; + bool enabled = true; + +public: + explicit RenderPass(const std::string& passName) : name(passName) {} + virtual ~RenderPass() = default; + + const std::string& GetName() const { return name; } + + void AddDependency(const std::string& dependency) { + dependencies.push_back(dependency); + } + + const std::vector& GetDependencies() const { + return dependencies; + } + + void SetRenderTarget(RenderTarget* renderTarget) { + target = renderTarget; + } + + RenderTarget* GetRenderTarget() const { + return target; + } + + void SetEnabled(bool isEnabled) { + enabled = isEnabled; + } + + bool IsEnabled() const { + return enabled; + } + + virtual void Execute(vk::raii::CommandBuffer& commandBuffer) { + if (!enabled) return; + + BeginPass(commandBuffer); + Render(commandBuffer); + EndPass(commandBuffer); + } + +protected: + // With dynamic rendering, BeginPass typically calls vkCmdBeginRendering + // instead of vkCmdBeginRenderPass + virtual void BeginPass(vk::raii::CommandBuffer& commandBuffer) = 0; + virtual void Render(vk::raii::CommandBuffer& commandBuffer) = 0; + // With dynamic rendering, EndPass typically calls vkCmdEndRendering + // instead of vkCmdEndRenderPass + virtual void EndPass(vk::raii::CommandBuffer& commandBuffer) = 0; +}; + +// Render target class +class RenderTarget { +private: + vk::raii::Image colorImage = nullptr; + vk::raii::DeviceMemory colorMemory = nullptr; + vk::raii::ImageView colorImageView = nullptr; + + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + + uint32_t width; + uint32_t height; + +public: + RenderTarget(uint32_t w, uint32_t h) : width(w), height(h) { + // Create color and depth images + CreateColorResources(); + CreateDepthResources(); + + // Note: With dynamic rendering, we don't need to create VkRenderPass + // or VkFramebuffer objects. Instead, we just create the images and + // image views that will be used directly with vkCmdBeginRendering. + } + + // No need for explicit destructor with RAII objects + + vk::ImageView GetColorImageView() const { return *colorImageView; } + vk::ImageView GetDepthImageView() const { return *depthImageView; } + + uint32_t GetWidth() const { return width; } + uint32_t GetHeight() const { return height; } + +private: + void CreateColorResources() { + // Implementation to create color image, memory, and view + // With dynamic rendering, we just need to create the image and image view + // that will be used with vkCmdBeginRendering + // ... + } + + void CreateDepthResources() { + // Implementation to create depth image, memory, and view + // With dynamic rendering, we just need to create the image and image view + // that will be used with vkCmdBeginRendering + // ... + } + + vk::raii::Device& GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // ... + static vk::raii::Device device = nullptr; // Placeholder + return device; + } +}; +---- + +=== Implementing Specific Render Passes + +Now let's implement some specific render passes: + +[source,cpp] +---- +// Geometry pass for deferred rendering +class GeometryPass : public RenderPass { +private: + CullingSystem* cullingSystem; + + // G-buffer textures + RenderTarget* gBuffer; + +public: + GeometryPass(const std::string& name, CullingSystem* culling) + : RenderPass(name), cullingSystem(culling) { + // Create G-buffer render target + gBuffer = new RenderTarget(1920, 1080); // Example resolution + SetRenderTarget(gBuffer); + } + + ~GeometryPass() override { + delete gBuffer; + } + +protected: + void BeginPass(vk::raii::CommandBuffer& commandBuffer) override { + // Begin rendering with dynamic rendering + vk::RenderingInfoKHR renderingInfo; + + // Set up color attachment + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(gBuffer->GetColorImageView()) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})); + + // Set up depth attachment + vk::RenderingAttachmentInfoKHR depthAttachment; + depthAttachment.setImageView(gBuffer->GetDepthImageView()) + .setImageLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearDepthStencilValue(1.0f, 0)); + + // Configure rendering info + renderingInfo.setRenderArea(vk::Rect2D({0, 0}, {gBuffer->GetWidth(), gBuffer->GetHeight()})) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment) + .setPDepthAttachment(&depthAttachment); + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + } + + void Render(vk::raii::CommandBuffer& commandBuffer) override { + // Get visible entities + const auto& visibleEntities = cullingSystem->GetVisibleEntities(); + + // Render each entity to G-buffer + for (auto entity : visibleEntities) { + auto meshComponent = entity->GetComponent(); + auto transformComponent = entity->GetComponent(); + + if (meshComponent && transformComponent) { + // Bind pipeline for G-buffer rendering + // ... + + // Set model matrix + // ... + + // Draw mesh + // ... + } + } + } + + void EndPass(vk::raii::CommandBuffer& commandBuffer) override { + // End dynamic rendering + commandBuffer.endRendering(); + } +}; + +// Lighting pass for deferred rendering +class LightingPass : public RenderPass { +private: + GeometryPass* geometryPass; + std::vector lights; + +public: + LightingPass(const std::string& name, GeometryPass* gPass) + : RenderPass(name), geometryPass(gPass) { + // Add dependency on geometry pass + AddDependency(gPass->GetName()); + } + + void AddLight(Light* light) { + lights.push_back(light); + } + + void RemoveLight(Light* light) { + auto it = std::find(lights.begin(), lights.end(), light); + if (it != lights.end()) { + lights.erase(it); + } + } + +protected: + void BeginPass(vk::raii::CommandBuffer& commandBuffer) override { + // Begin rendering with dynamic rendering + vk::RenderingInfoKHR renderingInfo; + + // Set up color attachment for the lighting pass + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(GetRenderTarget()->GetColorImageView()) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})); + + // Configure rendering info + renderingInfo.setRenderArea(vk::Rect2D({0, 0}, {GetRenderTarget()->GetWidth(), GetRenderTarget()->GetHeight()})) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment); + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + } + + void Render(vk::raii::CommandBuffer& commandBuffer) override { + // Bind G-buffer textures from the geometry pass + auto gBuffer = geometryPass->GetRenderTarget(); + + // Set up descriptor sets for G-buffer textures + // With dynamic rendering, we access the G-buffer textures directly as shader resources + // rather than as subpass inputs + + // Render full-screen quad with lighting shader + // ... + + // For each light + for (auto light : lights) { + // Set light properties + // ... + + // Draw light volume + // ... + } + } + + void EndPass(vk::raii::CommandBuffer& commandBuffer) override { + // End dynamic rendering + commandBuffer.endRendering(); + } +}; + +// Post-process effect base class +class PostProcessEffect { +public: + virtual ~PostProcessEffect() = default; + virtual void Apply(vk::raii::CommandBuffer& commandBuffer) = 0; +}; + +// Post-processing pass +class PostProcessPass : public RenderPass { +private: + LightingPass* lightingPass; + std::vector effects; + +public: + PostProcessPass(const std::string& name, LightingPass* lPass) + : RenderPass(name), lightingPass(lPass) { + // Add dependency on lighting pass + AddDependency(lPass->GetName()); + } + + void AddEffect(PostProcessEffect* effect) { + effects.push_back(effect); + } + + void RemoveEffect(PostProcessEffect* effect) { + auto it = std::find(effects.begin(), effects.end(), effect); + if (it != effects.end()) { + effects.erase(it); + } + } + +protected: + void BeginPass(vk::raii::CommandBuffer& commandBuffer) override { + // Begin rendering with dynamic rendering + vk::RenderingInfoKHR renderingInfo; + + // Set up color attachment for the post-processing pass + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(GetRenderTarget()->GetColorImageView()) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})); + + // Configure rendering info + renderingInfo.setRenderArea(vk::Rect2D({0, 0}, {GetRenderTarget()->GetWidth(), GetRenderTarget()->GetHeight()})) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment); + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + } + + void Render(vk::raii::CommandBuffer& commandBuffer) override { + // With dynamic rendering, each effect can set up its own rendering state + // and access input textures directly as shader resources + + // Apply each post-process effect + for (auto effect : effects) { + effect->Apply(commandBuffer); + } + } + + void EndPass(vk::raii::CommandBuffer& commandBuffer) override { + // End dynamic rendering + commandBuffer.endRendering(); + } +}; +---- + +=== Command Generation and Execution + +Once we have our render passes set up, we need to generate and execute commands: + +[source,cpp] +---- +class Renderer { +private: + vk::raii::Device device = nullptr; + vk::Queue graphicsQueue; + vk::raii::CommandPool commandPool = nullptr; + + RenderPassManager renderPassManager; + CullingSystem cullingSystem; + + // Current frame resources + vk::raii::CommandBuffer commandBuffer = nullptr; + vk::raii::Fence fence = nullptr; + vk::raii::Semaphore imageAvailableSemaphore = nullptr; + vk::raii::Semaphore renderFinishedSemaphore = nullptr; + +public: + Renderer(vk::raii::Device& dev, vk::Queue queue) : device(dev), graphicsQueue(queue) { + // Create command pool + // ... + + // Create synchronization objects + // ... + + // Set up render passes + SetupRenderPasses(); + } + + // No need for explicit destructor with RAII objects + + void SetCamera(Camera* camera) { + cullingSystem.SetCamera(camera); + } + + void Render(const std::vector& entities) { + // Wait for previous frame to finish + fence.wait(UINT64_MAX); + fence.reset(); + + // Reset command buffer + commandBuffer.reset(); + + // Perform culling + cullingSystem.CullScene(entities); + + // Record commands + vk::CommandBufferBeginInfo beginInfo; + commandBuffer.begin(beginInfo); + + // Execute render passes + renderPassManager.Execute(commandBuffer); + + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo; + + // With vk::raii, we need to dereference the command buffer + vk::CommandBuffer rawCommandBuffer = *commandBuffer; + submitInfo.setCommandBufferCount(1); + submitInfo.setPCommandBuffers(&rawCommandBuffer); + + // Set up wait and signal semaphores + vk::PipelineStageFlags waitStages[] = { vk::PipelineStageFlagBits::eColorAttachmentOutput }; + + // With vk::raii, we need to dereference the semaphores + vk::Semaphore rawImageAvailableSemaphore = *imageAvailableSemaphore; + vk::Semaphore rawRenderFinishedSemaphore = *renderFinishedSemaphore; + + submitInfo.setWaitSemaphoreCount(1); + submitInfo.setPWaitSemaphores(&rawImageAvailableSemaphore); + submitInfo.setPWaitDstStageMask(waitStages); + submitInfo.setSignalSemaphoreCount(1); + submitInfo.setPSignalSemaphores(&rawRenderFinishedSemaphore); + + // With vk::raii, we need to dereference the fence + vk::Fence rawFence = *fence; + graphicsQueue.submit(1, &submitInfo, rawFence); + } + +private: + void SetupRenderPasses() { + // Create geometry pass + auto geometryPass = renderPassManager.AddRenderPass("GeometryPass", &cullingSystem); + + // Create lighting pass + auto lightingPass = renderPassManager.AddRenderPass("LightingPass", geometryPass); + + // Create post-process pass + auto postProcessPass = renderPassManager.AddRenderPass("PostProcessPass", lightingPass); + + // Add post-process effects + // ... + } +}; +---- + +=== Advanced Rendering Techniques + +==== Deferred Rendering + +Deferred rendering separates the geometry and lighting calculations into separate passes, which can be more efficient for scenes with many lights: + +1. *Geometry Pass* - Render scene geometry to G-buffer textures (position, normal, albedo, etc.). +2. *Lighting Pass* - Apply lighting calculations using G-buffer textures. + +==== Forward+ Rendering + +Forward+ (or tiled forward) rendering combines the simplicity of forward rendering with some of the efficiency benefits of deferred rendering: + +1. *Light Culling Pass* - Divide the screen into tiles and determine which lights affect each tile. +2. *Forward Rendering Pass* - Render scene geometry with only the lights that affect each tile. + +==== Physically Based Rendering (PBR) + +PBR aims to create more realistic materials by simulating how light interacts with surfaces in the real world: + +1. *Material Parameters* - Define materials using physically meaningful parameters (albedo, metalness, roughness, etc.). +2. *BRDF* - Use a physically based bidirectional reflectance distribution function. +3. *Image-Based Lighting* - Use environment maps for ambient lighting. + +=== Conclusion + +A well-designed rendering pipeline is essential for creating a flexible and efficient rendering engine. By implementing the techniques described in this section, you can create a system that: + +1. Efficiently culls invisible objects +2. Organizes rendering into passes with clear dependencies +3. Supports advanced rendering techniques like deferred rendering and PBR +4. Can be easily extended with new effects and features + +In the next section, we'll explore event systems, which provide a flexible way for different parts of your engine to communicate with each other. + +link:04_resource_management.adoc[Previous: Resource Management] | link:06_event_systems.adoc[Next: Event Systems] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc new file mode 100644 index 00000000..7199ebeb --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc @@ -0,0 +1,542 @@ +:pp: {plus}{plus} + += Engine Architecture: Event Systems +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Event Systems + +Event systems provide a flexible way for different parts of your engine to communicate with each other without creating tight coupling. In this section, we'll explore how to design and implement an effective event system for your rendering engine. + +=== The Need for Event Systems + +In a complex engine, many subsystems need to communicate with each other: + +1. *Physics* needs to notify *Audio* when collisions occur. +2. *Input* needs to notify *Game Logic* when buttons are pressed. +3. *Game Logic* needs to notify *Rendering* when objects change. +4. *Resource Management* needs to notify *Rendering* when assets are loaded. + +Without an event system, these interactions would require direct references between subsystems, creating tight coupling and making the code harder to maintain and extend. + +=== Event System Design Principles + +When designing an event system, consider these principles: + +1. *Decoupling* - Minimize dependencies between event producers and consumers. +2. *Type Safety* - Use the type system to prevent errors. +3. *Performance* - Efficiently dispatch events, especially for high-frequency events. +4. *Flexibility* - Support different event delivery patterns (immediate, queued, etc.). +5. *Debugging* - Make it easy to debug event flow. + +=== Basic Event System Implementation + +Let's implement a basic event system: + +[source,cpp] +---- +// Base event class +class Event { +public: + virtual ~Event() = default; + + // Get the type of the event + virtual const char* GetType() const = 0; + + // Clone the event (for queued events) + virtual Event* Clone() const = 0; +}; + +// Macro to help define event types +#define DEFINE_EVENT_TYPE(type) \ + static const char* GetStaticType() { return #type; } \ + virtual const char* GetType() const override { return GetStaticType(); } \ + virtual Event* Clone() const override { return new type(*this); } + +// Example event types +class WindowResizeEvent : public Event { +private: + int width; + int height; + +public: + WindowResizeEvent(int w, int h) : width(w), height(h) {} + + int GetWidth() const { return width; } + int GetHeight() const { return height; } + + DEFINE_EVENT_TYPE(WindowResizeEvent) +}; + +class KeyPressEvent : public Event { +private: + int keyCode; + bool repeat; + +public: + KeyPressEvent(int key, bool isRepeat) : keyCode(key), repeat(isRepeat) {} + + int GetKeyCode() const { return keyCode; } + bool IsRepeat() const { return repeat; } + + DEFINE_EVENT_TYPE(KeyPressEvent) +}; + +// Event listener interface +class EventListener { +public: + virtual ~EventListener() = default; + virtual void OnEvent(const Event& event) = 0; +}; + +// Event dispatcher +class EventDispatcher { +private: + const Event& event; + +public: + explicit EventDispatcher(const Event& e) : event(e) {} + + // Dispatch event to handler if types match + template + bool Dispatch(const F& handler) { + if (event.GetType() == T::GetStaticType()) { + handler(static_cast(event)); + return true; + } + return false; + } +}; + +// Event bus +class EventBus { +private: + std::vector listeners; + std::queue> eventQueue; + std::mutex queueMutex; + bool immediateMode = true; + +public: + void SetImmediateMode(bool immediate) { + immediateMode = immediate; + } + + void AddListener(EventListener* listener) { + listeners.push_back(listener); + } + + void RemoveListener(EventListener* listener) { + auto it = std::find(listeners.begin(), listeners.end(), listener); + if (it != listeners.end()) { + listeners.erase(it); + } + } + + void PublishEvent(const Event& event) { + if (immediateMode) { + // Dispatch event immediately + for (auto listener : listeners) { + listener->OnEvent(event); + } + } else { + // Queue event for later processing + std::lock_guard lock(queueMutex); + eventQueue.push(std::unique_ptr(event.Clone())); + } + } + + void ProcessEvents() { + if (immediateMode) return; + + std::queue> currentEvents; + + { + std::lock_guard lock(queueMutex); + std::swap(currentEvents, eventQueue); + } + + while (!currentEvents.empty()) { + auto& event = *currentEvents.front(); + + for (auto listener : listeners) { + listener->OnEvent(event); + } + + currentEvents.pop(); + } + } +}; +---- + +=== Using the Event System + +Here's how you might use the event system in your application: + +[source,cpp] +---- +// Component that listens for events +class CameraController : public Component, public EventListener { +private: + CameraComponent* camera; + float moveSpeed = 5.0f; + float rotateSpeed = 0.1f; + + bool moveForward = false; + bool moveBackward = false; + bool moveLeft = false; + bool moveRight = false; + +public: + void Initialize() override { + camera = GetOwner()->GetComponent(); + + // Register as event listener + GetEventBus().AddListener(this); + } + + void Update(float deltaTime) override { + if (!camera) return; + + // Handle movement + glm::vec3 movement(0.0f); + + if (moveForward) movement.z -= 1.0f; + if (moveBackward) movement.z += 1.0f; + if (moveLeft) movement.x -= 1.0f; + if (moveRight) movement.x += 1.0f; + + if (glm::length(movement) > 0.0f) { + movement = glm::normalize(movement) * moveSpeed * deltaTime; + + auto transform = GetOwner()->GetComponent(); + if (transform) { + glm::vec3 position = transform->GetPosition(); + position += movement; + transform->SetPosition(position); + } + } + } + + void OnEvent(const Event& event) override { + EventDispatcher dispatcher(event); + + // Handle key press events + dispatcher.Dispatch([this](const KeyPressEvent& e) { + switch (e.GetKeyCode()) { + case KEY_W: moveForward = true; break; + case KEY_S: moveBackward = true; break; + case KEY_A: moveLeft = true; break; + case KEY_D: moveRight = true; break; + } + return false; + }); + + // Handle key release events + dispatcher.Dispatch([this](const KeyReleaseEvent& e) { + switch (e.GetKeyCode()) { + case KEY_W: moveForward = false; break; + case KEY_S: moveBackward = false; break; + case KEY_A: moveLeft = false; break; + case KEY_D: moveRight = false; break; + } + return false; + }); + + // Handle window resize events + dispatcher.Dispatch([this](const WindowResizeEvent& e) { + if (camera) { + float aspectRatio = static_cast(e.GetWidth()) / static_cast(e.GetHeight()); + camera->SetAspectRatio(aspectRatio); + } + return false; + }); + } + + ~CameraController() override { + // Unregister as event listener + GetEventBus().RemoveListener(this); + } + +private: + EventBus& GetEventBus() { + // Get event bus from somewhere (e.g., singleton or parameter) + static EventBus eventBus; + return eventBus; + } +}; + +// Input system that generates events +class InputSystem { +private: + EventBus& eventBus; + + // Key states + std::unordered_map keyStates; + +public: + explicit InputSystem(EventBus& bus) : eventBus(bus) {} + + void Update() { + // Poll input events from the platform + // ... + + // Example: Process a key press + ProcessKeyPress(KEY_W, false); + } + + void ProcessKeyPress(int keyCode, bool repeat) { + bool& keyState = keyStates[keyCode]; + + if (!keyState || repeat) { + // Key was not pressed before or this is a repeat + KeyPressEvent event(keyCode, repeat); + eventBus.PublishEvent(event); + } + + keyState = true; + } + + void ProcessKeyRelease(int keyCode) { + bool& keyState = keyStates[keyCode]; + + if (keyState) { + // Key was pressed before + KeyReleaseEvent event(keyCode); + eventBus.PublishEvent(event); + } + + keyState = false; + } +}; +---- + +=== Advanced Event System Features + +==== Event Categories + +Events can be categorized to allow listeners to filter which types of events they receive: + +[source,cpp] +---- +// Event categories +enum class EventCategory { + None = 0, + Application = 1 << 0, + Input = 1 << 1, + Keyboard = 1 << 2, + Mouse = 1 << 3, + MouseButton = 1 << 4, + Window = 1 << 5 +}; + +// Enhanced event base class +class Event { +public: + virtual ~Event() = default; + + virtual const char* GetType() const = 0; + virtual Event* Clone() const = 0; + + // Get the categories this event belongs to + virtual int GetCategoryFlags() const = 0; + + // Check if event is in category + bool IsInCategory(EventCategory category) const { + return GetCategoryFlags() & static_cast(category); + } +}; + +// Enhanced macro to define event types with categories +#define DEFINE_EVENT_TYPE_CATEGORY(type, categoryFlags) \ + static const char* GetStaticType() { return #type; } \ + virtual const char* GetType() const override { return GetStaticType(); } \ + virtual Event* Clone() const override { return new type(*this); } \ + virtual int GetCategoryFlags() const override { return categoryFlags; } + +// Example event with categories +class KeyPressEvent : public Event { +private: + int keyCode; + bool repeat; + +public: + KeyPressEvent(int key, bool isRepeat) : keyCode(key), repeat(isRepeat) {} + + int GetKeyCode() const { return keyCode; } + bool IsRepeat() const { return repeat; } + + DEFINE_EVENT_TYPE_CATEGORY(KeyPressEvent, + static_cast(EventCategory::Input) | + static_cast(EventCategory::Keyboard)) +}; +---- + +==== Event Filtering + +Listeners can filter events based on categories: + +[source,cpp] +---- +// Enhanced event bus with filtering +class EventBus { +private: + struct ListenerInfo { + EventListener* listener; + int categoryFilter; + }; + + std::vector listeners; + std::queue> eventQueue; + std::mutex queueMutex; + bool immediateMode = true; + +public: + void AddListener(EventListener* listener, int categoryFilter = -1) { + listeners.push_back({listener, categoryFilter}); + } + + void RemoveListener(EventListener* listener) { + auto it = std::find_if(listeners.begin(), listeners.end(), + [listener](const ListenerInfo& info) { + return info.listener == listener; + }); + if (it != listeners.end()) { + listeners.erase(it); + } + } + + void PublishEvent(const Event& event) { + if (immediateMode) { + // Dispatch event immediately + for (const auto& info : listeners) { + if (info.categoryFilter == -1 || (event.GetCategoryFlags() & info.categoryFilter)) { + info.listener->OnEvent(event); + } + } + } else { + // Queue event for later processing + std::lock_guard lock(queueMutex); + eventQueue.push(std::unique_ptr(event.Clone())); + } + } + + // Rest of the implementation... +}; +---- + +==== Event Priorities + +Some events may need to be processed before others: + +[source,cpp] +---- +// Enhanced event bus with priorities +class EventBus { +private: + struct ListenerInfo { + EventListener* listener; + int categoryFilter; + int priority; + }; + + std::vector listeners; + // Rest of the implementation... + +public: + void AddListener(EventListener* listener, int categoryFilter = -1, int priority = 0) { + listeners.push_back({listener, categoryFilter, priority}); + + // Sort listeners by priority (higher priority first) + std::sort(listeners.begin(), listeners.end(), + [](const ListenerInfo& a, const ListenerInfo& b) { + return a.priority > b.priority; + }); + } + + // Rest of the implementation... +}; +---- + +==== Event Bubbling and Capturing + +For hierarchical systems like UI, events can bubble up or capture down the hierarchy: + +[source,cpp] +---- +// UI event with bubbling +class UIEvent : public Event { +private: + UIElement* target; + bool bubbles; + bool cancelBubble = false; + +public: + UIEvent(UIElement* targetElement, bool bubbling = true) + : target(targetElement), bubbles(bubbling) {} + + UIElement* GetTarget() const { return target; } + bool Bubbles() const { return bubbles; } + + void StopPropagation() { + cancelBubble = true; + } + + bool IsPropagationStopped() const { + return cancelBubble; + } + + DEFINE_EVENT_TYPE_CATEGORY(UIEvent, static_cast(EventCategory::UI)) +}; + +// UI system with event bubbling +class UISystem { +public: + void DispatchEvent(UIEvent& event) { + UIElement* target = event.GetTarget(); + + // Capturing phase (top-down) + std::vector path; + UIElement* current = target; + + while (current) { + path.push_back(current); + current = current->GetParent(); + } + + // Dispatch to each element in the path (bottom-up) + for (auto it = path.rbegin(); it != path.rend(); ++it) { + (*it)->OnEvent(event); + + if (event.IsPropagationStopped()) { + break; + } + } + } +}; +---- + +=== Conclusion + +A well-designed event system is crucial for creating a flexible and maintainable engine architecture. By implementing the techniques described in this section, you can create a system that: + +1. Decouples subsystems, making your code more modular and easier to maintain +2. Provides type-safe event handling +3. Supports different event delivery patterns +4. Can be extended with advanced features like filtering, priorities, and bubbling + +This concludes our exploration of engine architecture. In this chapter, we've covered: + +1. Architectural patterns for structuring your engine +2. Component systems for building flexible game objects +3. Resource management for efficiently handling assets +4. Rendering pipeline design for flexible and efficient rendering +5. Event systems for decoupled communication between subsystems + +With these foundations in place, you're well-equipped to build a robust and flexible rendering engine that can be extended to support a wide range of features and techniques. + +link:05_rendering_pipeline.adoc[Previous: Rendering Pipeline] | link:conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc new file mode 100644 index 00000000..589bd76d --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc @@ -0,0 +1,66 @@ +:pp: {plus}{plus} + += Engine Architecture: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've explored the fundamental architectural patterns and design principles that form the backbone of a modern rendering engine. Let's recap what we've learned and discuss how to apply these concepts in your own engine development. + +=== What We've Covered + +Throughout this chapter, we've delved into several key aspects of engine architecture: + +1. *Architectural Patterns* - We examined common design patterns used in game and rendering engines, including layered architecture, component-based architecture, data-oriented design, and the service locator pattern. Each pattern offers different trade-offs in terms of flexibility, performance, and complexity. + +2. *Component Systems* - We implemented a flexible component-based architecture that allows for modular and reusable code. This approach promotes composition over inheritance, making it easier to create diverse game objects without deep inheritance hierarchies. + +3. *Resource Management* - We designed a robust resource management system that efficiently handles assets like textures, meshes, and shaders. Our system includes features like reference counting, caching, and hot reloading to optimize memory usage and improve development workflow. + +4. *Rendering Pipeline* - We structured a flexible rendering pipeline that can accommodate different rendering techniques and effects. Our pipeline includes stages for scene culling, render pass management, command generation, and post-processing. + +5. *Event Systems* - We implemented a decoupled communication system that allows different parts of the engine to interact without creating tight dependencies. Our event system supports features like event filtering, priorities, and bubbling. + +=== Applying These Concepts + +As you develop your own rendering engine, keep these principles in mind: + +1. *Start Simple* - Begin with a minimal implementation and add complexity as needed. It's easier to extend a simple, working system than to debug a complex one. + +2. *Focus on Interfaces* - Design clear interfaces between subsystems. This makes it easier to modify or replace individual components without affecting the rest of the engine. + +3. *Consider Performance Early* - While premature optimization should be avoided, certain architectural decisions can have significant performance implications that are difficult to change later. + +4. *Iterate and Refactor* - Your first design won't be perfect. Be prepared to refactor as you learn more about your specific requirements and constraints. + +5. *Balance Flexibility and Complexity* - More flexible systems often come with increased complexity. Find the right balance for your project's needs. + +=== Next Steps + +With a solid understanding of engine architecture, you're now ready to implement these concepts in your own rendering engine. Here are some suggestions for next steps: + +1. *Implement a Prototype* - Start by implementing a simple prototype that incorporates the key architectural patterns we've discussed. + +2. *Experiment with Different Approaches* - Try different variations of these patterns to see what works best for your specific needs. + +3. *Explore Advanced Topics* - Dive deeper into specific areas like advanced rendering techniques, physics integration, or audio systems. + +4. *Study Existing Engines* - Examine open-source engines to see how they solve similar problems. + +5. *Join the Community* - Engage with the graphics programming community to share ideas and get feedback on your approach. + +Remember that engine development is an iterative process. Your architecture will evolve as you gain experience and as your requirements change. The concepts we've covered provide a foundation, but the best architecture for your engine will depend on your specific goals and constraints. + +=== Final Thoughts + +Building a rendering engine is a challenging but rewarding endeavor. By applying the architectural patterns and design principles we've explored in this chapter, you'll be well-equipped to create a robust, flexible, and maintainable engine that can grow with your needs. + +Good luck with your engine development journey! + +link:06_event_systems.adoc[Previous: Event Systems] | link:../Camera_Transformations/01_introduction.adoc[Next: Camera Transformations] diff --git a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc new file mode 100644 index 00000000..d0d627e0 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc @@ -0,0 +1,43 @@ +::pp: {plus}{plus} + += GUI: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction + +Welcome to the "GUI" chapter of our "Building a Simple Engine" series! After implementing a camera system in the previous chapter, we'll now focus on adding a graphical user interface (GUI) to our Vulkan application. A well-designed GUI is essential for creating interactive applications that allow users to control settings, display information, and interact with the 3D scene. + +In this chapter, we'll integrate a popular immediate-mode GUI library called Dear ImGui with our Vulkan engine. ImGui is widely used in the game and graphics industry due to its simplicity, performance, and flexibility. It allows developers to quickly create debug interfaces, tools, and in-game menus without the complexity of traditional retained-mode GUI systems. + +In this chapter, we'll focus on: + +* Setting up Dear ImGui with Vulkan +* Creating and managing Vulkan resources for GUI rendering (buffers, textures, pipeline) +* Handling user input for both 3D navigation and GUI interaction +* Understanding key GUI integration concepts rather than exhaustive widget examples +* Integrating the GUI rendering with our Vulkan rendering pipeline +* Implementing object picking for 3D scene interaction + +By the end of this chapter, you'll have a functional GUI system that you can use to control your camera, adjust rendering settings, and interact with your 3D scene. This will serve as a foundation for more advanced features in later chapters, such as material editors, scene hierarchies, and debugging tools. + +== Prerequisites + +Before starting this chapter, you should have completed: + +* The Camera & Transformations chapter, as we'll build upon the camera system we developed there +* Solid understanding of Vulkan concepts, particularly: + ** Rendering pipeline and command buffers + ** Buffer and image creation + ** Descriptor sets and layouts + ** Pipeline creation +* Basic understanding of input handling concepts + +Let's begin by exploring how to implement a professional GUI system with Dear ImGui and Vulkan. + +link:../Lighting_Materials/06_conclusion.adoc[Previous: Lighting & Materials Conclusion] | link:02_imgui_setup.adoc[Next: Setting Up Dear ImGui] diff --git a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc new file mode 100644 index 00000000..1a5885ba --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc @@ -0,0 +1,696 @@ +::pp: {plus}{plus} + += GUI: Setting Up Dear ImGui +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Setting Up Dear ImGui + +In this section, we'll set up Dear ImGui in our Vulkan application. Dear ImGui (also known simply as ImGui) is a bloat-free graphical user interface library for C++. It outputs optimized vertex buffers that you can render with your 3D-pipeline-enabled application. It's particularly well-suited for integration with graphics APIs like Vulkan. + +=== Adding ImGui to Your Project + +First, we need to add ImGui to our project. There are several ways to do this: + +1. *Git Submodule*: Add ImGui as a Git submodule to your project +2. *Package Manager*: Use a package manager like vcpkg or Conan +3. *Manual Integration*: Download and include the ImGui source files directly + +For this tutorial, we'll use the manual integration approach for simplicity: + +[source,bash] +---- +# Clone ImGui repository +git clone https://github.com/ocornut/imgui.git external/imgui + +# Copy necessary files to your project +cp external/imgui/imgui.h include/ +cp external/imgui/imgui.cpp src/ +cp external/imgui/imgui_draw.cpp src/ +cp external/imgui/imgui_widgets.cpp src/ +cp external/imgui/imgui_tables.cpp src/ +cp external/imgui/imgui_demo.cpp src/ +---- + + +Next, update your CMakeLists.txt to include these files: + +[source,cmake] +---- +# ImGui files +set(IMGUI_SOURCES + src/imgui.cpp + src/imgui_draw.cpp + src/imgui_widgets.cpp + src/imgui_tables.cpp + src/imgui_demo.cpp +) + +# Our custom ImGui Vulkan integration +set(IMGUI_VULKAN_SOURCES + src/imgui_vulkan_util.cpp +) + +add_executable(VulkanApp + src/main.cpp + ${IMGUI_SOURCES} + ${IMGUI_VULKAN_SOURCES} +) + +target_include_directories(VulkanApp PRIVATE include) +---- + +=== Creating an ImGui Integration + +Let's implement the ImGuiVulkanUtil class to handle the integration between ImGui and Vulkan. + +Let's start by defining our ImGuiVulkanUtil class: + +[source,cpp] +---- +// ImGuiVulkanUtil.h +#pragma once + +#include +#include + +class ImGuiVulkanUtil { +private: + // Vulkan resources for rendering the UI + vk::raii::Sampler sampler{nullptr}; + Buffer vertexBuffer; + Buffer indexBuffer; + uint32_t vertexCount = 0; + uint32_t indexCount = 0; + Image fontImage; + ImageView fontImageView; + vk::raii::PipelineCache pipelineCache{nullptr}; + vk::raii::PipelineLayout pipelineLayout{nullptr}; + vk::raii::Pipeline pipeline{nullptr}; + vk::raii::DescriptorPool descriptorPool{nullptr}; + vk::raii::DescriptorSetLayout descriptorSetLayout{nullptr}; + vk::raii::DescriptorSet descriptorSet{nullptr}; + + // Device references + vk::raii::Device* device = nullptr; + vk::raii::PhysicalDevice* physicalDevice = nullptr; + vk::raii::Queue* graphicsQueue = nullptr; + uint32_t graphicsQueueFamily = 0; + + // UI style + ImGuiStyle vulkanStyle; + + // Push constants for UI rendering + struct PushConstBlock { + glm::vec2 scale; + glm::vec2 translate; + } pushConstBlock; + + // Flag to track if buffers need updating + bool needsUpdateBuffers = false; + + // Pipeline state for dynamic rendering + vk::PipelineRenderingCreateInfo renderingInfo{}; + vk::Format colorFormat = vk::Format::eB8G8R8A8Unorm; + +public: + ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Queue& graphicsQueue, uint32_t graphicsQueueFamily); + ~ImGuiVulkanUtil(); + + // Initialize ImGui context and style + void init(float width, float height); + + // Initialize all Vulkan resources + void initResources(); + + // Set UI style + void setStyle(uint32_t index); + + // Start a new ImGui frame + bool newFrame(); + + // Update vertex and index buffers + void updateBuffers(); + + // Draw ImGui elements to command buffer + void drawFrame(vk::raii::CommandBuffer& commandBuffer); + + // Input handling + void handleKey(int key, int scancode, int action, int mods); + bool getWantKeyCapture(); + void charPressed(uint32_t key); +}; +---- + +=== Implementing the ImGuiVulkanUtil Class + +Now let's implement the methods of our ImGuiVulkanUtil class for the Vulkan implementation. + +==== Constructor and Destructor + +First, let's implement the constructor and destructor: + +[source,cpp] +---- +ImGuiVulkanUtil::ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Queue& graphicsQueue, uint32_t graphicsQueueFamily) + : device(&device), physicalDevice(&physicalDevice), + graphicsQueue(&graphicsQueue), graphicsQueueFamily(graphicsQueueFamily), + // Initialize buffers directly + vertexBuffer(*device, 1, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent), + indexBuffer(*device, 1, + vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent) { + + // Set up dynamic rendering info + renderingInfo.colorAttachmentCount = 1; + vk::Format formats[] = { colorFormat }; + renderingInfo.pColorAttachmentFormats = formats; +} + +ImGuiVulkanUtil::~ImGuiVulkanUtil() { + // Wait for device to finish operations before destroying resources + if (device) { + device->waitIdle(); + } + + // All resources are automatically cleaned up by their destructors + // No manual cleanup needed + + // ImGui context is destroyed separately +} +---- + +==== Initialization + +Next, let's implement the initialization methods: + +[source,cpp] +---- +void ImGuiVulkanUtil::init(float width, float height) { + // Initialize ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + // Configure ImGui + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable keyboard controls + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable docking + + // Set display size + io.DisplaySize = ImVec2(width, height); + io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Set up style + vulkanStyle = ImGui::GetStyle(); + vulkanStyle.Colors[ImGuiCol_TitleBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.6f); + vulkanStyle.Colors[ImGuiCol_TitleBgActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); + vulkanStyle.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + vulkanStyle.Colors[ImGuiCol_Header] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + vulkanStyle.Colors[ImGuiCol_CheckMark] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); + + // Apply default style + setStyle(0); +} + +void ImGuiVulkanUtil::setStyle(uint32_t index) { + ImGuiStyle& style = ImGui::GetStyle(); + + switch (index) { + case 0: + // Custom Vulkan style + style = vulkanStyle; + break; + case 1: + // Classic style + ImGui::StyleColorsClassic(); + break; + case 2: + // Dark style + ImGui::StyleColorsDark(); + break; + case 3: + // Light style + ImGui::StyleColorsLight(); + break; + } +} +---- + +==== Resource Initialization + +Now let's implement the method to initialize all Vulkan resources needed for ImGui rendering: + +[source,cpp] +---- +void ImGuiVulkanUtil::initResources() { + // Create font texture + ImGuiIO& io = ImGui::GetIO(); + unsigned char* fontData; + int texWidth, texHeight; + io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight); + vk::DeviceSize uploadSize = texWidth * texHeight * 4 * sizeof(char); + + // Create the font image + vk::Extent3D fontExtent{ + static_cast(texWidth), + static_cast(texHeight), + 1 + }; + + // Create image for font texture + fontImage = Image(*device, fontExtent, vk::Format::eR8G8B8A8Unorm, + vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // Create image view + fontImageView = ImageView(*device, fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, + vk::ImageAspectFlagBits::eColor); + + // Upload font data to the image + Buffer stagingBuffer(*device, uploadSize, vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + void* data = stagingBuffer.map(); + memcpy(data, fontData, uploadSize); + stagingBuffer.unmap(); + + // Transition image layout and copy data + transitionImageLayout(fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + + copyBufferToImage(stagingBuffer.getHandle(), fontImage.getHandle(), + static_cast(texWidth), static_cast(texHeight)); + + transitionImageLayout(fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Create sampler for font texture + vk::SamplerCreateInfo samplerInfo{}; + samplerInfo.magFilter = vk::Filter::eLinear; + samplerInfo.minFilter = vk::Filter::eLinear; + samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; + samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; + + sampler = device->createSampler(samplerInfo); + + // Create descriptor pool + vk::DescriptorPoolSize poolSize{vk::DescriptorType::eCombinedImageSampler, 1}; + + vk::DescriptorPoolCreateInfo poolInfo{}; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; + poolInfo.maxSets = 2; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + + descriptorPool = device->createDescriptorPool(poolInfo); + + // Create descriptor set layout + vk::DescriptorSetLayoutBinding binding{}; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; + binding.descriptorCount = 1; + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + binding.binding = 0; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + + descriptorSetLayout = device->createDescriptorSetLayout(layoutInfo); + + // Allocate descriptor set + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = 1; + vk::DescriptorSetLayout layouts[] = {*descriptorSetLayout}; + allocInfo.pSetLayouts = layouts; + + descriptorSet = std::move(device->allocateDescriptorSets(allocInfo).front()); + + // Update descriptor set + vk::DescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + imageInfo.imageView = fontImageView.getHandle(); + imageInfo.sampler = *sampler; + + vk::WriteDescriptorSet writeSet{}; + writeSet.dstSet = *descriptorSet; + writeSet.descriptorCount = 1; + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; + writeSet.pImageInfo = &imageInfo; + writeSet.dstBinding = 0; + + device->updateDescriptorSets(1, &writeSet, 0, nullptr); + + // Create pipeline cache + vk::PipelineCacheCreateInfo pipelineCacheInfo{}; + pipelineCache = device->createPipelineCache(pipelineCacheInfo); + + // Create pipeline layout + vk::PushConstantRange pushConstantRange{}; + pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(PushConstBlock); + + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.setLayoutCount = 1; + vk::DescriptorSetLayout setLayouts[] = {*descriptorSetLayout}; + pipelineLayoutInfo.pSetLayouts = setLayouts; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; + + pipelineLayout = device->createPipelineLayout(pipelineLayoutInfo); + + // Create the graphics pipeline with dynamic rendering + // ... (shader loading, pipeline state setup, etc.) + + // For brevity, we're omitting the full pipeline creation code here + // In a real implementation, you would: + // 1. Load the vertex and fragment shaders + // 2. Set up all the pipeline state (vertex input, input assembly, rasterization, etc.) + // 3. Include the renderingInfo in the pipeline creation to enable dynamic rendering +} +---- + +==== Frame Management and Rendering + +Finally, let's implement the methods for frame management and rendering: + +[source,cpp] +---- +bool ImGuiVulkanUtil::newFrame() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Create your UI elements here + // For example: + ImGui::Begin("Vulkan ImGui Demo"); + ImGui::Text("Hello, Vulkan!"); + if (ImGui::Button("Click me!")) { + // Handle button click + } + ImGui::End(); + + // End the frame + ImGui::EndFrame(); + + // Render to generate draw data + ImGui::Render(); + + // Check if buffers need updating + ImDrawData* drawData = ImGui::GetDrawData(); + if (drawData && drawData->CmdListsCount > 0) { + if (drawData->TotalVtxCount > vertexCount || drawData->TotalIdxCount > indexCount) { + needsUpdateBuffers = true; + return true; + } + } + + return false; +} + +void ImGuiVulkanUtil::updateBuffers() { + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + // Calculate required buffer sizes + vk::DeviceSize vertexBufferSize = drawData->TotalVtxCount * sizeof(ImDrawVert); + vk::DeviceSize indexBufferSize = drawData->TotalIdxCount * sizeof(ImDrawIdx); + + // Resize buffers if needed + if (drawData->TotalVtxCount > vertexCount) { + // Recreate vertex buffer with new size + vertexBuffer = Buffer(*device, vertexBufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + vertexCount = drawData->TotalVtxCount; + } + + if (drawData->TotalIdxCount > indexCount) { + // Recreate index buffer with new size + indexBuffer = Buffer(*device, indexBufferSize, + vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + indexCount = drawData->TotalIdxCount; + } + + // Upload data to buffers + ImDrawVert* vtxDst = static_cast(vertexBuffer.map()); + ImDrawIdx* idxDst = static_cast(indexBuffer.map()); + + for (int n = 0; n < drawData->CmdListsCount; n++) { + const ImDrawList* cmdList = drawData->CmdLists[n]; + memcpy(vtxDst, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy(idxDst, cmdList->IdxBuffer.Data, cmdList->IdxBuffer.Size * sizeof(ImDrawIdx)); + vtxDst += cmdList->VtxBuffer.Size; + idxDst += cmdList->IdxBuffer.Size; + } + + vertexBuffer.unmap(); + indexBuffer.unmap(); +} + +void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + // Begin dynamic rendering + vk::RenderingAttachmentInfo colorAttachment{}; + // Note: In a real implementation, you would set the imageView, imageLayout, + // loadOp, storeOp, and clearValue based on your swapchain image + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, {static_cast(drawData->DisplaySize.x), + static_cast(drawData->DisplaySize.y)}}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + + commandBuffer.beginRendering(renderingInfo); + + // Bind the pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pipeline); + + // Set viewport + vk::Viewport viewport{}; + viewport.width = drawData->DisplaySize.x; + viewport.height = drawData->DisplaySize.y; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + commandBuffer.setViewport(0, viewport); + + // Set push constants + pushConstBlock.scale = glm::vec2(2.0f / drawData->DisplaySize.x, 2.0f / drawData->DisplaySize.y); + pushConstBlock.translate = glm::vec2(-1.0f); + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eVertex, + 0, sizeof(PushConstBlock), &pushConstBlock); + + // Bind vertex and index buffers + vk::Buffer vertexBuffers[] = { vertexBuffer.getHandle() }; + vk::DeviceSize offsets[] = { 0 }; + commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(indexBuffer.getHandle(), 0, vk::IndexType::eUint16); + + // Render command lists + int vertexOffset = 0; + int indexOffset = 0; + + for (int i = 0; i < drawData->CmdListsCount; i++) { + const ImDrawList* cmdList = drawData->CmdLists[i]; + + for (int j = 0; j < cmdList->CmdBuffer.Size; j++) { + const ImDrawCmd* pcmd = &cmdList->CmdBuffer[j]; + + // Set scissor rectangle + vk::Rect2D scissor{}; + scissor.offset.x = std::max(static_cast(pcmd->ClipRect.x), 0); + scissor.offset.y = std::max(static_cast(pcmd->ClipRect.y), 0); + scissor.extent.width = static_cast(pcmd->ClipRect.z - pcmd->ClipRect.x); + scissor.extent.height = static_cast(pcmd->ClipRect.w - pcmd->ClipRect.y); + commandBuffer.setScissor(0, scissor); + + // Bind descriptor set (font texture) + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *pipelineLayout, 0, *descriptorSet, {}); + + // Draw + commandBuffer.drawIndexed(pcmd->ElemCount, 1, indexOffset, vertexOffset, 0); + indexOffset += pcmd->ElemCount; + } + + vertexOffset += cmdList->VtxBuffer.Size; + } + + // End dynamic rendering + commandBuffer.endRendering(); +} +---- + +=== Input Handling + +Let's implement the input handling methods: + +[source,cpp] +---- +void ImGuiVulkanUtil::handleKey(int key, int scancode, int action, int mods) { + ImGuiIO& io = ImGui::GetIO(); + + // This example uses GLFW key codes and actions, but you can adapt this + // to work with any windowing library's input system + + // Map the platform-specific key action to ImGui's key state + // In GLFW: GLFW_PRESS = 1, GLFW_RELEASE = 0 + const int KEY_PRESSED = 1; // Generic key pressed value + const int KEY_RELEASED = 0; // Generic key released value + + if (action == KEY_PRESSED) + io.KeysDown[key] = true; + if (action == KEY_RELEASED) + io.KeysDown[key] = false; + + // Update modifier keys + // These key codes are GLFW-specific, but you would use your windowing library's + // equivalent key codes for other libraries + const int KEY_LEFT_CTRL = 341; // GLFW_KEY_LEFT_CONTROL + const int KEY_RIGHT_CTRL = 345; // GLFW_KEY_RIGHT_CONTROL + const int KEY_LEFT_SHIFT = 340; // GLFW_KEY_LEFT_SHIFT + const int KEY_RIGHT_SHIFT = 344; // GLFW_KEY_RIGHT_SHIFT + const int KEY_LEFT_ALT = 342; // GLFW_KEY_LEFT_ALT + const int KEY_RIGHT_ALT = 346; // GLFW_KEY_RIGHT_ALT + const int KEY_LEFT_SUPER = 343; // GLFW_KEY_LEFT_SUPER + const int KEY_RIGHT_SUPER = 347; // GLFW_KEY_RIGHT_SUPER + + io.KeyCtrl = io.KeysDown[KEY_LEFT_CTRL] || io.KeysDown[KEY_RIGHT_CTRL]; + io.KeyShift = io.KeysDown[KEY_LEFT_SHIFT] || io.KeysDown[KEY_RIGHT_SHIFT]; + io.KeyAlt = io.KeysDown[KEY_LEFT_ALT] || io.KeysDown[KEY_RIGHT_ALT]; + io.KeySuper = io.KeysDown[KEY_LEFT_SUPER] || io.KeysDown[KEY_RIGHT_SUPER]; +} + +bool ImGuiVulkanUtil::getWantKeyCapture() { + return ImGui::GetIO().WantCaptureKeyboard; +} + +void ImGuiVulkanUtil::charPressed(uint32_t key) { + ImGuiIO& io = ImGui::GetIO(); + io.AddInputCharacter(key); +} +---- + +=== Using the ImGuiVulkanUtil Class + +Now that we've implemented our ImGuiVulkanUtil class, let's see how to use it in a Vulkan application: + +[source,cpp] +---- +// In your application class +ImGuiVulkanUtil imGui; + +// During initialization +void initImGui() { + // Initialize ImGui directly + imGui = ImGuiVulkanUtil( + device, + physicalDevice, + graphicsQueue, + graphicsQueueFamily + ); + + imGui.init(swapChainExtent.width, swapChainExtent.height); + imGui.initResources(); // No renderPass needed with dynamic rendering +} + +// In your render loop +void drawFrame() { + // ... existing frame preparation code ... + + // Update ImGui + if (imGui.newFrame()) { + imGui.updateBuffers(); + } + + // Begin command buffer recording + // Note: With dynamic rendering, we don't need to begin a render pass + // The ImGui drawFrame method will handle dynamic rendering internally + + // Render scene using dynamic rendering + // ... + + // Render ImGui + imGui.drawFrame(commandBuffer); + + // ... submit command buffer ... +} + +// Input handling +// This example shows how to handle input with GLFW, but you can adapt this +// to work with any windowing library's input system + +// Example key callback function for GLFW +void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + // First check if ImGui wants to capture this input + imGui.handleKey(key, scancode, action, mods); + + // If ImGui doesn't want to capture the keyboard, process for your application + if (!imGui.getWantKeyCapture()) { + // Process key for your application + } +} + +// Example character input callback for GLFW +void charCallback(GLFWwindow* window, unsigned int codepoint) { + imGui.charPressed(codepoint); +} + +// With other windowing libraries, you would implement similar callback functions +// using their equivalent APIs and event systems + +// Cleanup +void cleanup() { + // ... existing cleanup code ... + + // ImGui will be automatically cleaned up when the application exits + // No manual cleanup needed +} +---- + +=== Testing the Integration + +To verify that our ImGui integration is working correctly, we can use the ImGui demo window, which showcases all of ImGui's features: + +[source,cpp] +---- +// In your ImGuiVulkanUtil::newFrame method +bool ImGuiVulkanUtil::newFrame() { + ImGui::NewFrame(); + + // Show the demo window + ImGui::ShowDemoWindow(); + + ImGui::EndFrame(); + ImGui::Render(); + + // Check if buffers need updating + // ... +} +---- + +With this implementation, you have a Vulkan implementation for ImGui that allows you to customize the rendering process to fit your specific needs. + +In the next section, we'll explore how to handle input for both the GUI and the 3D scene. + +link:01_introduction.adoc[Previous: Introduction] | link:03_input_handling.adoc[Next: Input Handling] diff --git a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc new file mode 100644 index 00000000..0eaa3ce6 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc @@ -0,0 +1,503 @@ +::pp: {plus}{plus} + += GUI: Input Handling +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Input Handling + +One of the challenges when integrating a GUI into a 3D application is managing input events. We need to ensure that input events are correctly routed to either the GUI or the 3D scene. For example, if the user is interacting with a UI element, we don't want their mouse movements to also rotate the camera. + +In this section, we'll explore how to handle input for both the GUI and the 3D scene, ensuring a smooth user experience regardless of the windowing system you choose to use. + +=== Creating a Platform-Agnostic Input System + +To create an effective input system that works with any windowing library, we need to abstract the input mechanisms and provide a clean interface. Let's define a simple input system that can be adapted to different platforms: + +[source,cpp] +---- +// InputSystem.h +#pragma once + +#include +#include +#include +#include + +// Input actions that our application can respond to +enum class InputAction { + MOVE_FORWARD, + MOVE_BACKWARD, + MOVE_LEFT, + MOVE_RIGHT, + MOVE_UP, + MOVE_DOWN, + LOOK_UP, + LOOK_DOWN, + LOOK_LEFT, + LOOK_RIGHT, + ZOOM_IN, + ZOOM_OUT, + TOGGLE_UI_MODE, + // Add more actions as needed +}; + +// Input state that tracks the current state of inputs +struct InputState { + glm::vec2 cursorPosition = {0.0f, 0.0f}; + glm::vec2 cursorDelta = {0.0f, 0.0f}; + bool mouseButtons[3] = {false, false, false}; + float scrollDelta = 0.0f; + + // For touch input + struct TouchPoint { + int id; + glm::vec2 position; + glm::vec2 delta; + }; + std::vector touchPoints; + + // Reset delta values after each frame + void resetDeltas() { + cursorDelta = {0.0f, 0.0f}; + scrollDelta = 0.0f; + for (auto& touch : touchPoints) { + touch.delta = {0.0f, 0.0f}; + } + } +}; + +class InputSystem { +public: + static void Initialize(); + static void Shutdown(); + + // Update input state (called once per frame) + static void Update(float deltaTime); + + // Register a callback for an input action + static void RegisterActionCallback(InputAction action, std::function callback); + + // Process a platform-specific input event + static bool ProcessInputEvent(void* event); + + // Get the current input state + static const InputState& GetInputState(); + + // Check if ImGui is capturing input + static bool IsImGuiCapturingKeyboard(); + static bool IsImGuiCapturingMouse(); + +private: + static InputState inputState; + static std::unordered_map> actionCallbacks; +}; +---- + +=== Input Prioritization + +The general approach for input handling in applications with both 3D navigation and GUI is: + +1. First, check if the GUI is capturing input (e.g., mouse is over a UI element) +2. If the GUI is not capturing input, then process the input for 3D navigation + +Let's implement this approach using our cross-platform input system: + +[source,cpp] +---- +void processInput(float deltaTime) { + // Check if ImGui is capturing keyboard input + bool imguiCapturingKeyboard = InputSystem::IsImGuiCapturingKeyboard(); + + // Check if ImGui is capturing mouse input + bool imguiCapturingMouse = InputSystem::IsImGuiCapturingMouse(); + + // Get the current input state + const InputState& inputState = InputSystem::GetInputState(); + + // Process keyboard input for camera movement if ImGui is not capturing keyboard + if (!imguiCapturingKeyboard) { + // Forward these to the camera system + // This could be done through the action callback system + if (InputSystem::IsActionActive(InputAction::MOVE_FORWARD)) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_BACKWARD)) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_LEFT)) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_RIGHT)) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_UP)) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_DOWN)) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); + } + + // Process mouse/touch input for camera rotation if ImGui is not capturing mouse + if (!imguiCapturingMouse) { + if (inputState.cursorDelta.x != 0.0f || inputState.cursorDelta.y != 0.0f) { + camera.processMouseMovement(inputState.cursorDelta.x, -inputState.cursorDelta.y); + } + + if (inputState.scrollDelta != 0.0f) { + camera.processMouseScroll(inputState.scrollDelta); + } + } +} +---- + +=== Platform-Specific Input Implementation + +Our platform-agnostic input system needs specific implementations for each windowing library to capture input events. Here's an example implementation using GLFW, a popular windowing library: + +==== Example: GLFW Implementation + +[source,cpp] +---- +// InputSystem_GLFW.cpp + +#include "InputSystem.h" +#include +#include + +// Store the GLFW window pointer +static GLFWwindow* gWindow = nullptr; +static bool mouseCaptureMode = false; + +// GLFW callback functions +static void glfwMouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { + if (button >= 0 && button < 3) { + InputState& state = InputSystem::GetInputState(); + state.mouseButtons[button] = action == GLFW_PRESS; + } +} + +static void glfwCursorPosCallback(GLFWwindow* window, double xpos, double ypos) { + InputState& state = InputSystem::GetInputState(); + + // Calculate delta from last position + glm::vec2 newPos(static_cast(xpos), static_cast(ypos)); + state.cursorDelta = newPos - state.cursorPosition; + state.cursorPosition = newPos; +} + +static void glfwScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + InputState& state = InputSystem::GetInputState(); + state.scrollDelta = static_cast(yoffset); +} + +static void glfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + // Map GLFW keys to our input actions + if (action == GLFW_PRESS || action == GLFW_RELEASE) { + bool pressed = (action == GLFW_PRESS); + + // Toggle mouse capture mode with Escape key + if (key == GLFW_KEY_ESCAPE && pressed) { + mouseCaptureMode = !mouseCaptureMode; + + if (mouseCaptureMode) { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } else { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + } + + // Map other keys to actions + // ... + } +} + +void InputSystem::Initialize(GLFWwindow* window) { + gWindow = window; + + // Set up GLFW callbacks + glfwSetMouseButtonCallback(window, glfwMouseButtonCallback); + glfwSetCursorPosCallback(window, glfwCursorPosCallback); + glfwSetScrollCallback(window, glfwScrollCallback); + glfwSetKeyCallback(window, glfwKeyCallback); + + // Initially capture the cursor for camera control + mouseCaptureMode = true; + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} + +void InputSystem::Update(float deltaTime) { + // Poll for input events + glfwPollEvents(); + + // Update key states for continuous actions (like movement) + if (glfwGetKey(gWindow, GLFW_KEY_W) == GLFW_PRESS) { + if (auto it = actionCallbacks.find(InputAction::MOVE_FORWARD); it != actionCallbacks.end()) { + it->second(deltaTime); + } + } + + // ... other keys ... + + // Reset delta values after processing + inputState.resetDeltas(); +} + +bool InputSystem::IsImGuiCapturingKeyboard() { + return ImGui::GetIO().WantCaptureKeyboard; +} + +bool InputSystem::IsImGuiCapturingMouse() { + return ImGui::GetIO().WantCaptureMouse; +} + +---- + + +=== Input Modes + +For applications that need different input modes (e.g., camera control vs. UI interaction), we can implement a mode system: + +[source,cpp] +---- +// Define input modes +enum class InputMode { + CAMERA_CONTROL, + UI_INTERACTION, + OBJECT_MANIPULATION +}; + +// Current input mode +static InputMode currentInputMode = InputMode::CAMERA_CONTROL; + +// Set the input mode +void setInputMode(InputMode mode) { + currentInputMode = mode; + + // Update platform-specific settings based on the mode + // This example shows how to implement this with GLFW + if (mode == InputMode::CAMERA_CONTROL) { + // In GLFW, we can disable the cursor for camera control + glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } else { + // For UI interaction, we want the cursor to be visible + glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + + // With other windowing libraries, you would use their equivalent APIs +} + +// Toggle between camera control and UI interaction modes +void toggleInputMode() { + if (currentInputMode == InputMode::CAMERA_CONTROL) { + setInputMode(InputMode::UI_INTERACTION); + } else { + setInputMode(InputMode::CAMERA_CONTROL); + } +} +---- + +=== Handling GUI-Specific Input + +Some GUI interactions might require special handling. For example, you might want to implement drag-and-drop functionality or custom keyboard shortcuts for UI elements: + +[source,cpp] +---- +void drawGUI() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Create a window for camera controls + ImGui::Begin("Camera Controls"); + + // Add a button to reset camera position + if (ImGui::Button("Reset Camera")) { + camera.setPosition(glm::vec3(0.0f, 0.0f, 3.0f)); + camera.setYaw(-90.0f); + camera.setPitch(0.0f); + } + + // Add sliders for camera settings + float movementSpeed = camera.getMovementSpeed(); + if (ImGui::SliderFloat("Movement Speed", &movementSpeed, 1.0f, 10.0f)) { + camera.setMovementSpeed(movementSpeed); + } + + float sensitivity = camera.getMouseSensitivity(); + if (ImGui::SliderFloat("Mouse Sensitivity", &sensitivity, 0.1f, 1.0f)) { + camera.setMouseSensitivity(sensitivity); + } + + float zoom = camera.getZoom(); + if (ImGui::SliderFloat("Zoom", &zoom, 1.0f, 45.0f)) { + camera.setZoom(zoom); + } + + ImGui::End(); + + // Render ImGui + ImGui::Render(); +} +---- + +=== Integrating Input Handling with the Main Loop + +Finally, let's integrate our input handling system with the main loop: + +[source,cpp] +---- +void mainLoop() { + // Main application loop + while (isRunning) { + // Calculate delta time + float deltaTime = calculateDeltaTime(); + + // Update input system + InputSystem::Update(deltaTime); + + // Process input for camera and other systems + processInput(deltaTime); + + // Draw GUI + drawGUI(); + + // Update uniform buffer with latest camera data + updateUniformBuffer(currentFrame); + + // Draw frame + drawFrame(); + } +} +---- + +=== Main Loop Integration + +The input system needs to be integrated with your application's main loop. Here's an example of how to do this with GLFW, but similar principles apply to other windowing libraries: + +[source,cpp] +---- +// Example main loop with GLFW +void runMainLoop() { + // Initialize input system with your window + // With GLFW, this would look like: + InputSystem::Initialize(window); + + // Main loop - with GLFW, we check if the window should close + // Other libraries would have their own condition + while (!glfwWindowShouldClose(window)) { + float deltaTime = calculateDeltaTime(); + + // Update input and process events + // This would be platform-specific + InputSystem::Update(deltaTime); + + // Rest of the main loop is platform-independent + processInput(deltaTime); + drawGUI(); + updateUniformBuffer(currentFrame); + drawFrame(); + } +} +---- + + +=== Advanced Input Handling Techniques + +For more complex applications, you might want to consider these advanced input handling techniques: + +==== Gesture Recognition + +Gesture recognition can enhance the user experience regardless of which windowing library you use: + +[source,cpp] +---- +// GestureRecognizer.h +#pragma once + +#include +#include +#include + +enum class GestureType { + TAP, + DOUBLE_TAP, + LONG_PRESS, + SWIPE, + PINCH, + ROTATE, + PAN +}; + +struct GestureEvent { + GestureType type; + glm::vec2 position; + glm::vec2 delta; + float scale; // For pinch + float rotation; // For rotate + int pointerCount; +}; + +class GestureRecognizer { +public: + static void Initialize(); + static void Update(const InputState& inputState, float deltaTime); + + // Register callbacks for different gesture types + static void RegisterGestureCallback(GestureType type, std::function callback); + +private: + static void detectTap(const InputState& inputState); + static void detectSwipe(const InputState& inputState); + static void detectPinch(const InputState& inputState); + static void detectRotate(const InputState& inputState); + static void detectPan(const InputState& inputState); + + static std::unordered_map> gestureCallbacks; +}; +---- + + +==== Input Context System + +For more complex applications with different input requirements in different states: + +[source,cpp] +---- +// InputContext.h +#pragma once + +#include +#include +#include +#include + +class InputContext { +public: + // Create a new input context + static void CreateContext(const std::string& name); + + // Push a context onto the stack (making it active) + static void PushContext(const std::string& name); + + // Pop the top context from the stack + static void PopContext(); + + // Get the current active context + static std::string GetActiveContext(); + + // Register an action handler for a specific context + static void RegisterActionHandler(const std::string& contextName, InputAction action, std::function handler); + + // Process an action in the current context + static void ProcessAction(InputAction action, float deltaTime); + +private: + static std::unordered_map>> contextHandlers; + static std::stack contextStack; +}; +---- + + +With these advanced input handling techniques, your application can provide a consistent and intuitive user experience. In the next section, we'll explore how to create various UI elements to control your application. + +link:02_imgui_setup.adoc[Previous: Setting Up Dear ImGui] | link:04_ui_elements.adoc[Next: UI Elements] diff --git a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc new file mode 100644 index 00000000..3c70791a --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc @@ -0,0 +1,602 @@ +::pp: {plus}{plus} + += GUI: UI Elements and Integration Concepts +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== UI Elements and Integration Concepts + +Now that we have set up ImGui and implemented input handling, let's explore the key concepts of integrating a GUI with your Vulkan application. We'll focus on the integration aspects rather than exhaustive ImGui widget examples, as those are well-documented in the ImGui documentation. + +=== GUI Integration Concepts + +When integrating a GUI into a 3D application, there are several important concepts to consider: + +1. *Separation of Concerns*: Keep your GUI code separate from your rendering code to maintain clean architecture. +2. *Performance Impact*: GUIs can impact performance, especially with complex layouts or frequent updates. +3. *Input Management*: Properly handle input to ensure it's routed to either the GUI or the 3D scene. +4. *Rendering Order*: The GUI is typically rendered after the 3D scene, as an overlay. +5. *State Management*: Use the GUI to modify application state in a controlled manner. + +=== Basic ImGui Usage + +ImGui follows an immediate-mode paradigm, where the UI is recreated every frame. Here's a simple example: + +[source,cpp] +---- +void drawGUI() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Create a window + ImGui::Begin("Settings"); + + // Add UI elements here + static bool enableFeature = false; + if (ImGui::Checkbox("Enable Feature", &enableFeature)) { + // This code runs when the checkbox value changes + updateFeatureState(enableFeature); + } + + static float value = 0.5f; + if (ImGui::SliderFloat("Parameter", &value, 0.0f, 1.0f)) { + // This code runs when the slider value changes + updateParameter(value); + } + + ImGui::End(); + + // Render ImGui + ImGui::Render(); +} +---- + +For a comprehensive guide to all available ImGui widgets and their options, please refer to the official ImGui documentation and demo: +https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp + +=== GUI Design Considerations for Vulkan Applications + +When designing a GUI for your Vulkan application, consider these aspects: + +==== Memory Management + +ImGui generates vertex and index buffers that need to be uploaded to the GPU. Ensure these resources are properly managed: + +1. *Buffer Sizing*: Allocate buffers with sufficient size or implement resizing logic +2. *Memory Types*: Use host-visible memory for frequent updates +3. *Synchronization*: Ensure buffer updates are synchronized with rendering + +==== Command Buffer Integration + +Integrate ImGui rendering commands with your Vulkan command buffers: + +[source,cpp] +---- +// Record commands for scene rendering +// ... + +// Record ImGui rendering commands +imGuiUtil.drawFrame(commandBuffer); + +// Submit command buffer +// ... +---- + +==== Descriptor Resources + +ImGui requires descriptors for its font texture. Ensure your descriptor pool has sufficient capacity: + +[source,cpp] +---- +// Create descriptor pool with enough capacity for ImGui +vk::DescriptorPoolSize poolSizes[] = { + { vk::DescriptorType::eCombinedImageSampler, 1000 }, + // Other descriptor types... +}; + +vk::DescriptorPoolCreateInfo poolInfo{}; +poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; +poolInfo.maxSets = 1000; +poolInfo.poolSizeCount = static_cast(std::size(poolSizes)); +poolInfo.pPoolSizes = poolSizes; + +descriptorPool = device.createDescriptorPool(poolInfo); +---- + +=== Performance Considerations + +When integrating ImGui with Vulkan, consider these performance aspects: + +1. *Command Buffer Recording*: Record ImGui commands efficiently, ideally once per frame +2. *Descriptor Management*: Minimize descriptor set allocations and updates +3. *Buffer Updates*: Optimize vertex and index buffer updates +4. *Pipeline State*: Use a dedicated pipeline for ImGui to minimize state changes +5. *Render Pass Integration*: Consider whether to use a separate render pass or subpass for the GUI + +=== Organizing Your GUI Code + +For maintainable GUI code, consider these organizational patterns: + +1. *Component-Based Approach*: Split your GUI into logical components +2. *State Management*: Use a centralized state store that the GUI can modify +3. *Event System*: Implement an event system for GUI-triggered actions +4. *Lazy Updates*: Only update Vulkan resources when GUI settings actually change + +[source,cpp] +---- +// Component-based approach example +class VulkanGUI { +private: + // GUI state + struct { + bool showRenderSettings = true; + bool showPerformance = true; + bool showSceneControls = true; + } state; + + // Components + void drawRenderSettingsPanel(); + void drawPerformancePanel(); + void drawSceneControlsPanel(); + +public: + void draw() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Draw components based on state + if (state.showRenderSettings) drawRenderSettingsPanel(); + if (state.showPerformance) drawPerformancePanel(); + if (state.showSceneControls) drawSceneControlsPanel(); + + // Main menu for toggling panels + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("Render Settings", nullptr, &state.showRenderSettings); + ImGui::MenuItem("Performance", nullptr, &state.showPerformance); + ImGui::MenuItem("Scene Controls", nullptr, &state.showSceneControls); + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } + + // Render ImGui + ImGui::Render(); + } +}; +---- + +=== Displaying Textures in ImGui + +A common requirement in GUI systems is displaying textures, such as rendered scenes, material previews, or icons. ImGui provides the ability to display textures through its `ImGui::Image` and `ImGui::ImageButton` functions. To use these with Vulkan, you need to properly set up descriptor sets for your textures. + +==== Setting Up Texture Descriptors + +To display a Vulkan texture in ImGui, you need to: + +1. Create a descriptor set layout for the texture +2. Allocate a descriptor set +3. Update the descriptor set with your texture's image view and sampler +4. Pass the descriptor set handle to ImGui + +Here's how to set up the necessary resources: + +[source,cpp] +---- +// Create a descriptor set layout for textures +vk::DescriptorSetLayoutBinding binding{}; +binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; +binding.descriptorCount = 1; +binding.stageFlags = vk::ShaderStageFlagBits::eFragment; +binding.binding = 0; + +vk::DescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.bindingCount = 1; +layoutInfo.pBindings = &binding; + +vk::raii::DescriptorSetLayout textureSetLayout = device.createDescriptorSetLayout(layoutInfo); + +// Allocate a descriptor set for each texture +vk::DescriptorSetAllocateInfo allocInfo{}; +allocInfo.descriptorPool = *descriptorPool; +allocInfo.descriptorSetCount = 1; +vk::DescriptorSetLayout layouts[] = {*textureSetLayout}; +allocInfo.pSetLayouts = layouts; + +vk::raii::DescriptorSet textureDescriptorSet = std::move(device.allocateDescriptorSets(allocInfo).front()); + +// Update the descriptor set with your texture +vk::DescriptorImageInfo imageInfo{}; +imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; +imageInfo.imageView = textureImageView.getHandle(); +imageInfo.sampler = *textureSampler; + +vk::WriteDescriptorSet writeSet{}; +writeSet.dstSet = *textureDescriptorSet; +writeSet.descriptorCount = 1; +writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; +writeSet.pImageInfo = &imageInfo; +writeSet.dstBinding = 0; + +device.updateDescriptorSets(1, &writeSet, 0, nullptr); +---- + +==== Using Textures in ImGui + +Once you have set up the descriptor set, you can use it with ImGui's image functions: + +[source,cpp] +---- +// Store the descriptor set as ImTextureID (which is just a void*) +ImTextureID textureId = (ImTextureID)(VkDescriptorSet)*textureDescriptorSet; + +// Display the texture in ImGui +ImGui::Begin("Texture Viewer"); + +// Display as a simple image +ImGui::Image(textureId, ImVec2(width, height)); + +// Or as an image button +if (ImGui::ImageButton(textureId, ImVec2(width, height))) { + // Handle button click +} + +// You can also apply tinting and modify UV coordinates +ImGui::Image(textureId, ImVec2(width, height), + ImVec2(0, 0), ImVec2(1, 1), // UV coordinates (0,0) to (1,1) for the full texture + ImVec4(1, 1, 1, 1), // Tint color (white = no tint) + ImVec4(1, 1, 1, 0.5)); // Border color + +ImGui::End(); +---- + +==== Complete Example: Texture Manager for ImGui + +Here's a more complete example of a texture manager class that handles multiple textures for ImGui: + +[source,cpp] +---- +class ImGuiTextureManager { +private: + vk::raii::Device* device = nullptr; + vk::raii::DescriptorPool* descriptorPool = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout{nullptr}; + + struct TextureInfo { + vk::raii::DescriptorSet descriptorSet{nullptr}; + uint32_t width; + uint32_t height; + }; + + std::unordered_map textures; + +public: + ImGuiTextureManager(vk::raii::Device& device, vk::raii::DescriptorPool& descriptorPool) + : device(&device), descriptorPool(&descriptorPool) { + + // Create descriptor set layout for textures + vk::DescriptorSetLayoutBinding binding{}; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; + binding.descriptorCount = 1; + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + binding.binding = 0; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + + descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo); + } + + // Register a texture for use with ImGui + ImTextureID registerTexture(const std::string& name, vk::ImageView imageView, + vk::Sampler sampler, uint32_t width, uint32_t height) { + + // Allocate descriptor set + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = **descriptorPool; + allocInfo.descriptorSetCount = 1; + vk::DescriptorSetLayout layouts[] = {*descriptorSetLayout}; + allocInfo.pSetLayouts = layouts; + + vk::raii::DescriptorSet descriptorSet = std::move(device->allocateDescriptorSets(allocInfo).front()); + + // Update descriptor set + vk::DescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + imageInfo.imageView = imageView; + imageInfo.sampler = sampler; + + vk::WriteDescriptorSet writeSet{}; + writeSet.dstSet = *descriptorSet; + writeSet.descriptorCount = 1; + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; + writeSet.pImageInfo = &imageInfo; + writeSet.dstBinding = 0; + + device->updateDescriptorSets(1, &writeSet, 0, nullptr); + + // Store texture info + textures[name] = {std::move(descriptorSet), width, height}; + + // Return the descriptor set as ImTextureID + return (ImTextureID)(VkDescriptorSet)*textures[name].descriptorSet; + } + + // Get a previously registered texture + ImTextureID getTexture(const std::string& name) { + if (textures.find(name) == textures.end()) { + throw std::runtime_error("Texture not found: " + name); + } + + return (ImTextureID)(VkDescriptorSet)*textures[name].descriptorSet; + } + + // Get texture dimensions + ImVec2 getTextureDimensions(const std::string& name) { + if (textures.find(name) == textures.end()) { + throw std::runtime_error("Texture not found: " + name); + } + + return ImVec2(static_cast(textures[name].width), + static_cast(textures[name].height)); + } +}; +---- + +==== Usage Example + +Here's how you might use the texture manager in your application: + +[source,cpp] +---- +// During initialization +ImGuiTextureManager textureManager(device, descriptorPool); + +// Register textures (e.g., after loading a model or rendering to a texture) +ImTextureID albedoTexId = textureManager.registerTexture( + "albedo", + albedoImageView, + textureSampler, + albedoWidth, + albedoHeight +); + +ImTextureID normalMapId = textureManager.registerTexture( + "normalMap", + normalMapImageView, + textureSampler, + normalMapWidth, + normalMapHeight +); + +// In your GUI rendering code +void drawMaterialEditor() { + ImGui::Begin("Material Editor"); + + // Display textures + ImGui::Text("Albedo Texture:"); + ImGui::Image(textureManager.getTexture("albedo"), + ImVec2(200, 200)); + + ImGui::Text("Normal Map:"); + ImGui::Image(textureManager.getTexture("normalMap"), + ImVec2(200, 200)); + + // Material properties + static float roughness = 0.5f; + if (ImGui::SliderFloat("Roughness", &roughness, 0.0f, 1.0f)) { + updateMaterialProperty("roughness", roughness); + } + + static float metallic = 0.0f; + if (ImGui::SliderFloat("Metallic", &metallic, 0.0f, 1.0f)) { + updateMaterialProperty("metallic", metallic); + } + + ImGui::End(); +} +---- + +==== Performance Considerations + +When working with textures in ImGui, keep these performance considerations in mind: + +1. *Descriptor Management*: Create descriptor sets for textures only when needed and reuse them +2. *Texture Size*: Consider using smaller preview versions of textures for the UI +3. *Mipmap Selection*: For large textures, ensure proper mipmap selection to avoid aliasing +4. *Texture Updates*: If a texture changes frequently, use a staging buffer for updates +5. *Texture Atlas*: For many small textures (like icons), consider using a texture atlas + +By properly managing textures in your ImGui integration, you can create rich interfaces that display rendered content, material previews, and other visual elements directly in your GUI. + +=== Object Picking: Interacting with the 3D Scene + +An important aspect of GUI integration is handling object picking - selecting 3D objects with the mouse. This requires coordination between ImGui and your 3D scene: + +[source,cpp] +---- +void handleMouseInput(float mouseX, float mouseY) { + // First, check if ImGui is using this input + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureMouse) { + // ImGui is using the mouse, don't use it for 3D picking + return; + } + + // ImGui isn't using the mouse, so we can use it for 3D picking + pickObject(mouseX, mouseY); +} + +void pickObject(float mouseX, float mouseY) { + // Convert screen coordinates to normalized device coordinates + float ndcX = (2.0f * mouseX) / windowWidth - 1.0f; + float ndcY = 1.0f - (2.0f * mouseY) / windowHeight; + + // Create a ray from the camera through the mouse position + glm::vec4 clipCoords(ndcX, ndcY, -1.0f, 1.0f); + glm::vec4 eyeCoords = glm::inverse(projectionMatrix) * clipCoords; + eyeCoords = glm::vec4(eyeCoords.x, eyeCoords.y, -1.0f, 0.0f); + + glm::vec3 rayDirection = glm::normalize(glm::vec3( + glm::inverse(viewMatrix) * eyeCoords + )); + + glm::vec3 rayOrigin = camera.getPosition(); + + // Test for intersections with scene objects + float closestHit = std::numeric_limits::max(); + int hitObjectId = -1; + + for (size_t i = 0; i < sceneObjects.size(); i++) { + float hitDistance; + if (rayIntersectsObject(rayOrigin, rayDirection, sceneObjects[i], hitDistance)) { + if (hitDistance < closestHit) { + closestHit = hitDistance; + hitObjectId = static_cast(i); + } + } + } + + // If we hit an object, select it + if (hitObjectId >= 0) { + selectObject(hitObjectId); + } +} +---- + +==== Implementing Ray-Object Intersection + +For object picking to work, you need to implement ray-object intersection tests. Here's a simple example for sphere intersection: + +[source,cpp] +---- +bool rayIntersectsSphere( + const glm::vec3& rayOrigin, + const glm::vec3& rayDirection, + const glm::vec3& sphereCenter, + float sphereRadius, + float& outDistance +) { + glm::vec3 oc = rayOrigin - sphereCenter; + float a = glm::dot(rayDirection, rayDirection); + float b = 2.0f * glm::dot(oc, rayDirection); + float c = glm::dot(oc, oc) - sphereRadius * sphereRadius; + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + return false; // No intersection + } + + // Calculate the closest intersection point + float t = (-b - sqrt(discriminant)) / (2.0f * a); + if (t < 0) { + // Try the other intersection point + t = (-b + sqrt(discriminant)) / (2.0f * a); + if (t < 0) { + return false; // Both intersection points are behind the ray + } + } + + outDistance = t; + return true; +} +---- + +==== Visualizing Selected Objects + +Once an object is selected, you can visualize the selection: + +[source,cpp] +---- +void drawScene(vk::raii::CommandBuffer& commandBuffer) { + // Draw all objects + for (size_t i = 0; i < sceneObjects.size(); i++) { + // If this object is selected, use a different pipeline + if (static_cast(i) == selectedObjectId) { + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *highlightPipeline); + } else { + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *standardPipeline); + } + + // Draw the object + drawObject(commandBuffer, sceneObjects[i]); + } +} +---- + +==== Integrating Picking with ImGui + +You can also display information about the selected object in the GUI: + +[source,cpp] +---- +void drawObjectPropertiesPanel() { + if (selectedObjectId < 0) { + return; // No object selected + } + + ImGui::Begin("Object Properties"); + + SceneObject& obj = sceneObjects[selectedObjectId]; + + // Display object properties + ImGui::Text("Object ID: %d", selectedObjectId); + ImGui::Text("Name: %s", obj.name.c_str()); + + // Edit object properties + glm::vec3 position = obj.position; + if (ImGui::DragFloat3("Position", &position[0], 0.1f)) { + obj.position = position; + updateObjectTransform(selectedObjectId); + } + + glm::vec3 rotation = obj.rotation; + if (ImGui::DragFloat3("Rotation", &rotation[0], 1.0f, -180.0f, 180.0f)) { + obj.rotation = rotation; + updateObjectTransform(selectedObjectId); + } + + glm::vec3 scale = obj.scale; + if (ImGui::DragFloat3("Scale", &scale[0], 0.1f, 0.1f, 10.0f)) { + obj.scale = scale; + updateObjectTransform(selectedObjectId); + } + + ImGui::End(); +} +---- + +Object picking creates a powerful interaction model where users can select and manipulate 3D objects directly, while using the GUI to fine-tune properties. This combination of direct manipulation and precise control provides an intuitive user experience. + +=== Balancing GUI and 3D Interaction + +When designing your application, consider how to balance GUI-based controls with direct 3D interaction: + +1. *Use GUI for*: + - Precise numerical inputs + - Complex settings with many options + - Hierarchical data visualization + - Application-wide controls + +2. *Use 3D Interaction for*: + - Object placement and movement + - Camera navigation + - Direct manipulation of scene elements + - Intuitive spatial operations + +3. *Hybrid Approaches*: + - Gizmos for 3D transformation with precise control + - Context menus that appear near selected objects + - Property panels that update based on selection + +By thoughtfully integrating ImGui with your Vulkan application and implementing object picking, you can create a powerful and intuitive user interface that combines the strengths of both 2D GUI controls and direct 3D interaction. + +In the next section, we'll explore more details about integrating the GUI rendering with the Vulkan rendering pipeline. + +link:03_input_handling.adoc[Previous: Input Handling] | link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc new file mode 100644 index 00000000..6b64c5b3 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc @@ -0,0 +1,854 @@ +::pp: {plus}{plus} + += GUI: Vulkan Integration +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Vulkan Integration + +In this section, we'll explore how to properly integrate ImGui rendering with the Vulkan rendering pipeline. While we've already covered the basic setup in the "Setting Up Dear ImGui" section, here we'll dive deeper into the technical details of how ImGui works with Vulkan and how to optimize the integration. + +=== Understanding the Rendering Flow + +Before we dive into the implementation details, let's understand how ImGui rendering fits into the Vulkan rendering pipeline: + +1. *Prepare Frame*: Begin a new frame in ImGui and create UI elements +2. *Generate Draw Data*: ImGui generates vertex and index buffers for the UI +3. *Record Commands*: Record Vulkan commands to render the ImGui draw data +4. *Submit Commands*: Submit the commands to the Vulkan queue +5. *Present*: Present the rendered frame to the screen + +This flow needs to be integrated with your existing Vulkan rendering pipeline, which typically involves: + +1. Acquiring the next swap chain image +2. Recording command buffers for scene rendering +3. Submitting command buffers +4. Presenting the rendered image + +=== Dynamic Rendering Configuration + +ImGui can be integrated with Vulkan's dynamic rendering feature, which simplifies the rendering process by eliminating the need for explicit render passes and framebuffers: + +[source,cpp] +---- +// When initializing ImGui, we set up our custom Vulkan renderer with dynamic rendering +ImGuiVulkanRenderer renderer; +// ... configure the renderer ... +renderer.initialize(*device, *physicalDevice); + +// Set up dynamic rendering info +vk::PipelineRenderingCreateInfo renderingInfo{}; +renderingInfo.colorAttachmentCount = 1; +vk::Format formats[] = { vk::Format::eB8G8R8A8Unorm }; +renderingInfo.pColorAttachmentFormats = formats; +renderer.setDynamicRenderingInfo(renderingInfo); +---- + +Dynamic rendering simplifies the integration by removing the dependency on render passes and framebuffers, making the code more flexible and easier to maintain. + +=== Command Buffer Integration + +There are two main approaches to integrating ImGui commands with your Vulkan command buffers: + +1. *Single Command Buffer*: Record both scene and ImGui rendering commands in the same command buffer +2. *Multiple Command Buffers*: Use separate command buffers for scene and ImGui rendering + +Let's look at both approaches: + +==== Single Command Buffer Approach + +This is the simplest approach and works well for most applications. With dynamic rendering, the code becomes even cleaner: + +[source,cpp] +---- +void drawFrame() { + // ... existing frame preparation code ... + + // Start recording command buffer + vk::CommandBufferBeginInfo beginInfo{}; + commandBuffer.begin(beginInfo); + + // Begin dynamic rendering for scene + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; + + vk::RenderingAttachmentInfo depthAttachment{}; + depthAttachment.imageView = *depthImageView; + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + renderingInfo.pDepthAttachment = &depthAttachment; + + commandBuffer.beginRendering(renderingInfo); + + // Render 3D scene + // ... your existing scene rendering code ... + + commandBuffer.endRendering(); + + // Render ImGui using our custom renderer + // ImGui will handle its own dynamic rendering internally + renderer.render(ImGui::GetDrawData(), commandBuffer); + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + // ... your existing submission code ... +} +---- + +==== Multiple Command Buffers Approach + +This approach gives you more flexibility and can be useful for more complex rendering pipelines. With dynamic rendering, it becomes even more straightforward: + +[source,cpp] +---- +void drawFrame() { + // ... existing frame preparation code ... + + // Record scene command buffer + vk::CommandBufferBeginInfo beginInfo{}; + sceneCommandBuffer.begin(beginInfo); + + // Begin dynamic rendering for scene + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; + + vk::RenderingAttachmentInfo depthAttachment{}; + depthAttachment.imageView = *depthImageView; + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + renderingInfo.pDepthAttachment = &depthAttachment; + + sceneCommandBuffer.beginRendering(renderingInfo); + + // Render 3D scene + // ... your existing scene rendering code ... + + sceneCommandBuffer.endRendering(); + sceneCommandBuffer.end(); + + // Record ImGui command buffer + imguiCommandBuffer.begin(beginInfo); + + // For ImGui, we want to preserve the contents of the previous rendering + colorAttachment.loadOp = vk::AttachmentLoadOp::eLoad; + + // No need for depth attachment for UI + renderingInfo.pDepthAttachment = nullptr; + + // Render ImGui using our custom renderer + // ImGui will handle its own dynamic rendering internally + renderer.render(ImGui::GetDrawData(), imguiCommandBuffer); + + imguiCommandBuffer.end(); + + // Submit command buffers in order + std::array submitCommandBuffers = { + *sceneCommandBuffer, + *imguiCommandBuffer + }; + + vk::SubmitInfo submitInfo{}; + submitInfo.commandBufferCount = static_cast(submitCommandBuffers.size()); + submitInfo.pCommandBuffers = submitCommandBuffers.data(); + + // ... rest of your submission code ... +} +---- + +=== Handling Multiple Viewports + +ImGui supports multiple viewports, which allows UI windows to be detached from the main window. To support this feature, we need to handle additional steps: + +[source,cpp] +---- +// In your main loop, after rendering ImGui +if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); +} +---- + +This will render any detached ImGui windows. Note that this feature requires additional platform-specific code and may not be necessary for all applications. + +=== Handling Window Resize + +When the window is resized, you need to recreate the swap chain and update ImGui: + +[source,cpp] +---- +void recreateSwapChain() { + // ... existing swap chain recreation code ... + + // Update ImGui display size + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(swapChainExtent.width), + static_cast(swapChainExtent.height)); +} +---- + +=== Performance Considerations + +Here are some tips to optimize ImGui rendering performance in Vulkan: + +1. *Minimize State Changes*: Try to render all ImGui elements in a single pass to minimize state changes. + +2. *Use Appropriate Descriptor Pool Sizes*: Allocate enough descriptors for ImGui to avoid running out of descriptors. + +3. *Consider Secondary Command Buffers*: For complex UIs, consider using secondary command buffers to record ImGui commands in parallel. + +4. *Optimize UI Updates*: Only update UI elements that change, and consider using ImGui's `Begin()` function with the `ImGuiWindowFlags_NoDecoration` flag for static UI elements. + +5. *Use ImGui's Memory Allocators*: ImGui allows you to provide custom memory allocators, which can be useful for controlling memory usage. + +=== Complete Integration Example + +Let's put everything together in a complete example that integrates ImGui with a Vulkan application: + +[source,cpp] +---- +class VulkanApplication { +private: + // ... existing Vulkan members ... + + // ImGui-specific members + vk::raii::DescriptorPool imguiPool = nullptr; + bool showDemoWindow = true; + bool showMetricsWindow = false; + +public: + void initVulkan() { + // ... existing Vulkan initialization ... + + // Initialize ImGui + createImGuiDescriptorPool(); + initImGui(); + } + + void createImGuiDescriptorPool() { + vk::DescriptorPoolSize poolSizes[] = + { + { vk::DescriptorType::eSampler, 1000 }, + { vk::DescriptorType::eCombinedImageSampler, 1000 }, + { vk::DescriptorType::eSampledImage, 1000 }, + { vk::DescriptorType::eStorageImage, 1000 }, + { vk::DescriptorType::eUniformTexelBuffer, 1000 }, + { vk::DescriptorType::eStorageTexelBuffer, 1000 }, + { vk::DescriptorType::eUniformBuffer, 1000 }, + { vk::DescriptorType::eStorageBuffer, 1000 }, + { vk::DescriptorType::eUniformBufferDynamic, 1000 }, + { vk::DescriptorType::eStorageBufferDynamic, 1000 }, + { vk::DescriptorType::eInputAttachment, 1000 } + }; + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = 1000, + .poolSizeCount = static_cast(std::size(poolSizes)), + .pPoolSizes = poolSizes + }; + + imguiPool = vk::raii::DescriptorPool(device, poolInfo); + } + + void initImGui() { + // Initialize ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Initialize our custom backend + int width = static_cast(swapChainExtent.width); + int height = static_cast(swapChainExtent.height); + ImGuiPlatform::Init(width, height); + + // Initialize our custom ImGui Vulkan renderer with dynamic rendering + ImGuiVulkanRenderer renderer; + renderer.initialize( + *instance, + *physicalDevice, + *device, + graphicsFamily, + *graphicsQueue, + *imguiPool, + static_cast(swapChainImages.size()), + vk::SampleCountFlagBits::e1 + ); + + // Set up dynamic rendering info + vk::PipelineRenderingCreateInfo renderingInfo{}; + renderingInfo.colorAttachmentCount = 1; + vk::Format formats[] = { swapChainImageFormat }; + renderingInfo.pColorAttachmentFormats = formats; + renderer.setDynamicRenderingInfo(renderingInfo); + + // Upload ImGui fonts + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + renderer.uploadFonts(commandBuffer); + endSingleTimeCommands(commandBuffer); + } + + void drawFrame() { + // ... existing frame preparation code ... + + // Start the ImGui frame + ImGui::NewFrame(); + + // Create ImGui UI + createImGuiUI(); + + // Render ImGui + ImGui::Render(); + + // ... existing command buffer recording code ... + + // Begin dynamic rendering for scene + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; + + vk::RenderingAttachmentInfo depthAttachment{}; + depthAttachment.imageView = *depthImageView; + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + renderingInfo.pDepthAttachment = &depthAttachment; + + commandBuffer.beginRendering(renderingInfo); + + // Render 3D scene + // ... your existing scene rendering code ... + + commandBuffer.endRendering(); + + // Render ImGui using our custom renderer + // ImGui will handle its own dynamic rendering internally + renderer.render(ImGui::GetDrawData(), commandBuffer); + + // ... existing command buffer submission code ... + } + + void createImGuiUI() { + // Menu bar + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Exit", "Alt+F4")) { + // Generic way to request application exit + requestApplicationExit(); + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("Demo Window", nullptr, &showDemoWindow); + ImGui::MenuItem("Metrics", nullptr, &showMetricsWindow); + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } + + // Demo window + if (showDemoWindow) { + ImGui::ShowDemoWindow(&showDemoWindow); + } + + // Metrics window + if (showMetricsWindow) { + ImGui::ShowMetricsWindow(&showMetricsWindow); + } + + // Custom windows + ImGui::Begin("Settings"); + + static float color[3] = { 0.5f, 0.5f, 0.5f }; + if (ImGui::ColorEdit3("Clear Color", color)) { + // Update clear color + clearColor = { color[0], color[1], color[2], 1.0f }; + } + + static int selectedModel = 0; + const char* models[] = { "Cube", "Sphere", "Teapot", "Custom Model" }; + if (ImGui::Combo("Model", &selectedModel, models, IM_ARRAYSIZE(models))) { + // Change model + loadModel(models[selectedModel]); + } + + ImGui::End(); + } + + void cleanup() { + // ... existing cleanup code ... + + // Cleanup ImGui + renderer.cleanup(); + ImGuiPlatform::Shutdown(); // Our custom platform backend + ImGui::DestroyContext(); + } +}; +---- + +=== Advanced Topics + +==== Custom Shaders for ImGui + +ImGui uses its own shaders for rendering, but you can customize them if needed: + +[source,cpp] +---- +// Create custom shader modules +vk::raii::ShaderModule customVertShaderModule = createShaderModule("custom_imgui_vert.spv"); +vk::raii::ShaderModule customFragShaderModule = createShaderModule("custom_imgui_frag.spv"); + +// Initialize our custom renderer with custom shaders and dynamic rendering +ImGuiVulkanRenderer renderer; +renderer.initialize( + *instance, + *physicalDevice, + *device, + queueFamily, + *queue, + *descriptorPool, + minImageCount, + imageCount, + vk::SampleCountFlagBits::e1 +); + +// Set up dynamic rendering info +vk::PipelineRenderingCreateInfo renderingInfo{}; +renderingInfo.colorAttachmentCount = 1; +vk::Format formats[] = { swapChainImageFormat }; +renderingInfo.pColorAttachmentFormats = formats; +renderer.setDynamicRenderingInfo(renderingInfo); + +// Set custom shaders +renderer.setCustomShaders( + customVertShaderModule, + customFragShaderModule +); +---- + +==== Rendering ImGui to a Texture + +You can render ImGui to a texture instead of directly to the screen, which can be useful for creating in-game UI elements: + +[source,cpp] +---- +// Create a texture to render ImGui to +vk::raii::Image imguiTargetImage = createImage( + width, height, + vk::Format::eR8G8B8A8Unorm, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled +); + +// Create image view +vk::raii::ImageView imguiTargetImageView = createImageView( + imguiTargetImage, + vk::Format::eR8G8B8A8Unorm, + vk::ImageAspectFlagBits::eColor +); + +// Render ImGui to the texture using dynamic rendering +vk::RenderingAttachmentInfo colorAttachment{}; +colorAttachment.imageView = *imguiTargetImageView; +colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; +colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; +colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; +colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 0.0f}; + +vk::RenderingInfo renderingInfo{}; +renderingInfo.renderArea = vk::Rect2D{{0, 0}, {width, height}}; +renderingInfo.layerCount = 1; +renderingInfo.colorAttachmentCount = 1; +renderingInfo.pColorAttachments = &colorAttachment; + +commandBuffer.beginRendering(renderingInfo); +renderer.render(ImGui::GetDrawData(), commandBuffer); +commandBuffer.endRendering(); + +// Later, use the texture in your 3D scene +// ... +---- + +==== Handling High DPI Displays + +For high DPI displays, you need to handle scaling correctly across different platforms: + +[source,cpp] +---- +// Cross-platform display scaling +void updateDisplayScale(int width, int height, float scaleX, float scaleY) { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(scaleX, scaleY); + + // Update our platform backend + ImGuiPlatform::SetDisplaySize(width, height); +} + +// Platform-specific implementations +// Here's an example using GLFW, but you can implement similar functions +// for any windowing library you choose to use + +void updateDisplayScaleWithGLFW(GLFWwindow* window) { + // Get the framebuffer size (which may differ from window size on high DPI displays) + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + // Get the content scale (DPI scaling factor) + float xscale, yscale; + glfwGetWindowContentScale(window, &xscale, &yscale); + + // Update ImGui with the correct display size and scale + updateDisplayScale(width, height, xscale, yscale); +} + +// With other windowing libraries, you would use their equivalent APIs +// to get the framebuffer size and DPI scaling factor + +---- + +=== ImGui Utility Class + +To encapsulate all the ImGui functionality in a way that works across different platforms, let's create a utility class similar to the one mentioned in the Vulkan-Samples repository: + +[source,cpp] +---- +// ImGuiUtil.h +#pragma once + +import vulkan_hpp; +#include +#include +#include + +class ImGuiUtil { +public: + // Initialize ImGui with Vulkan using dynamic rendering + static void Init( + vk::raii::Instance& instance, + vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Device& device, + uint32_t queueFamily, + vk::raii::Queue& queue, + uint32_t minImageCount, + uint32_t imageCount, + vk::Format swapChainImageFormat, + vk::SampleCountFlagBits msaaSamples = vk::SampleCountFlagBits::e1 + ); + + // Shutdown ImGui + static void Shutdown(); + + // Start a new frame + static void NewFrame(); + + // Render ImGui draw data to a command buffer + static void Render(vk::raii::CommandBuffer& commandBuffer); + + // Update display size + static void UpdateDisplaySize(int width, int height, float scaleX = 1.0f, float scaleY = 1.0f); + + // Process platform-specific input event + static bool ProcessInputEvent(void* event); + + // Set input callback + static void SetInputCallback(std::function callback); + +private: + // Create descriptor pool for ImGui + static void createDescriptorPool(); + + // Upload fonts + static void uploadFonts(); + + // Begin single-time commands + static vk::raii::CommandBuffer beginSingleTimeCommands(); + + // End single-time commands + static void endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer); + + // Vulkan objects + static vk::raii::Instance* instance; + static vk::raii::PhysicalDevice* physicalDevice; + static vk::raii::Device* device; + static uint32_t queueFamily; + static vk::raii::Queue* queue; + static vk::raii::DescriptorPool descriptorPool; + static vk::raii::CommandPool commandPool; + static vk::PipelineRenderingCreateInfo renderingInfo; + + // Input callback + static std::function inputCallback; + + // Initialization state + static bool initialized; +}; + +// ImGuiUtil.cpp +#include "ImGuiUtil.h" + +// Static member initialization +vk::raii::Instance* ImGuiUtil::instance = nullptr; +vk::raii::PhysicalDevice* ImGuiUtil::physicalDevice = nullptr; +vk::raii::Device* ImGuiUtil::device = nullptr; +uint32_t ImGuiUtil::queueFamily = 0; +vk::raii::Queue* ImGuiUtil::queue = nullptr; +vk::raii::DescriptorPool ImGuiUtil::descriptorPool = nullptr; +vk::raii::CommandPool ImGuiUtil::commandPool = nullptr; +vk::PipelineRenderingCreateInfo ImGuiUtil::renderingInfo{}; +std::function ImGuiUtil::inputCallback = nullptr; +bool ImGuiUtil::initialized = false; + +void ImGuiUtil::Init( + vk::raii::Instance& instance, + vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Device& device, + uint32_t queueFamily, + vk::raii::Queue& queue, + uint32_t minImageCount, + uint32_t imageCount, + vk::Format swapChainImageFormat, + vk::SampleCountFlagBits msaaSamples +) { + ImGuiUtil::instance = &instance; + ImGuiUtil::physicalDevice = &physicalDevice; + ImGuiUtil::device = &device; + ImGuiUtil::queueFamily = queueFamily; + ImGuiUtil::queue = &queue; + + // Set up dynamic rendering info + renderingInfo.colorAttachmentCount = 1; + vk::Format formats[] = { swapChainImageFormat }; + renderingInfo.pColorAttachmentFormats = formats; + + // Create command pool for font upload + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient, + .queueFamilyIndex = queueFamily + }; + commandPool = vk::raii::CommandPool(device, poolInfo); + + // Create descriptor pool + createDescriptorPool(); + + // Initialize ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Initialize our custom Vulkan renderer with dynamic rendering + renderer = ImGuiVulkanRenderer(); + renderer.initialize( + *instance, + *physicalDevice, + *device, + queueFamily, + *queue, + *descriptorPool, + minImageCount, + imageCount, + msaaSamples + ); + + // Set dynamic rendering info + renderer.setDynamicRenderingInfo(renderingInfo); + + // Upload fonts + uploadFonts(); + + initialized = true; +} + +void ImGuiUtil::Shutdown() { + if (!initialized) return; + + // Wait for device to finish operations + device->waitIdle(); + + // Cleanup ImGui + renderer.cleanup(); + ImGui::DestroyContext(); + + // Cleanup Vulkan resources + commandPool = nullptr; + descriptorPool = nullptr; + + // Reset pointers + instance = nullptr; + physicalDevice = nullptr; + device = nullptr; + queue = nullptr; + + initialized = false; +} + +void ImGuiUtil::NewFrame() { + if (!initialized) return; + + // Update ImGui IO with platform-specific input + ImGuiIO& io = ImGui::GetIO(); + + // Call input callback if registered + if (inputCallback) { + inputCallback(io); + } + + ImGui::NewFrame(); +} + +void ImGuiUtil::Render(vk::raii::CommandBuffer& commandBuffer) { + if (!initialized) return; + + ImGui::Render(); + renderer.render(ImGui::GetDrawData(), commandBuffer); +} + +void ImGuiUtil::UpdateDisplaySize(int width, int height, float scaleX, float scaleY) { + if (!initialized) return; + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(scaleX, scaleY); +} + +bool ImGuiUtil::ProcessInputEvent(void* event) { + // Platform-specific event processing would go here + // This is a placeholder for the actual implementation + return false; +} + +void ImGuiUtil::SetInputCallback(std::function callback) { + inputCallback = callback; +} + +void ImGuiUtil::createDescriptorPool() { + vk::DescriptorPoolSize poolSizes[] = + { + { vk::DescriptorType::eSampler, 1000 }, + { vk::DescriptorType::eCombinedImageSampler, 1000 }, + { vk::DescriptorType::eSampledImage, 1000 }, + { vk::DescriptorType::eStorageImage, 1000 }, + { vk::DescriptorType::eUniformTexelBuffer, 1000 }, + { vk::DescriptorType::eStorageTexelBuffer, 1000 }, + { vk::DescriptorType::eUniformBuffer, 1000 }, + { vk::DescriptorType::eStorageBuffer, 1000 }, + { vk::DescriptorType::eUniformBufferDynamic, 1000 }, + { vk::DescriptorType::eStorageBufferDynamic, 1000 }, + { vk::DescriptorType::eInputAttachment, 1000 } + }; + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = 1000, + .poolSizeCount = static_cast(std::size(poolSizes)), + .pPoolSizes = poolSizes + }; + + descriptorPool = vk::raii::DescriptorPool(*device, poolInfo); +} + +void ImGuiUtil::uploadFonts() { + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + renderer.uploadFonts(commandBuffer); + endSingleTimeCommands(commandBuffer); +} + +vk::raii::CommandBuffer ImGuiUtil::beginSingleTimeCommands() { + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffer commandBuffer = vk::raii::CommandBuffers(*device, allocInfo).front(); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + return commandBuffer; +} + +void ImGuiUtil::endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer) { + commandBuffer.end(); + + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + queue->submit(submitInfo); + queue->waitIdle(); +} +---- + +=== Conclusion + +In this section, we've explored how to integrate ImGui with Vulkan, including command buffer integration, render pass configuration, and performance considerations. By creating a flexible implementation, we've ensured that our GUI system works well with any windowing system you choose. + +The key improvements we've made include: + +1. Creating a platform-agnostic integration approach +2. Implementing a flexible input system that works with various windowing libraries +3. Developing a versatile ImGui utility class +4. Designing a window-system-independent integration + +With this knowledge, you can create a robust GUI system for your Vulkan application that provides a smooth user experience regardless of which windowing system you use. + +In the next section, we'll wrap up with a conclusion and discuss potential improvements to our GUI system. + +link:04_ui_elements.adoc[Previous: UI Elements] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc new file mode 100644 index 00000000..23795eb3 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc @@ -0,0 +1,132 @@ +::pp: {plus}{plus} + += GUI: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've built a comprehensive GUI system for our Vulkan application using Dear ImGui. Let's summarize what we've learned and discuss potential improvements. + +=== What We've Learned + +* *Flexible ImGui Setup*: We explored how to integrate Dear ImGui with Vulkan in a way that works across different platforms, including desktop and mobile. We created an implementation that doesn't rely on specific windowing systems like GLFW. + +* *Versatile Input Handling*: We implemented a robust input handling system that correctly routes input events to either the GUI or the 3D scene, ensuring a smooth user experience on any device. + +* *UI Elements*: We learned how to create various UI elements, from basic components like buttons and sliders to more complex elements like tables and plots, and how to organize them into a cohesive interface that works well on both desktop and mobile platforms. + +* *Vulkan Integration*: We dove deep into the technical details of integrating ImGui with the Vulkan rendering pipeline, including command buffer integration, render pass configuration, and performance considerations. + +With these components in place, we now have a solid foundation for creating interactive applications with Vulkan that can run on multiple platforms. Our GUI system allows users to control settings, display information, and interact with the 3D scene through an intuitive interface, whether they're using a desktop computer, a mobile phone, or a tablet. + +=== Potential Improvements + +While our GUI system is functional, there are several ways it could be enhanced: + +* *Targeted Optimizations*: Implement specific optimizations for better performance on each target platform. + +* *Touch-Friendly UI*: Enhance the UI elements to be more touch-friendly for mobile platforms, with larger hit areas and gesture support. + +* *Adaptive Layouts*: Create layouts that automatically adapt to different screen sizes and orientations, from desktop monitors to mobile phones. + +* *Custom Styling*: Create a custom theme that matches your application's visual style, rather than using the default ImGui style. + +* *Localization*: Add support for multiple languages by implementing a localization system for UI text. + +* *Accessibility*: Improve accessibility by adding features like keyboard navigation, screen reader support, and high-contrast modes. + +* *Persistent Settings*: Implement a system to save and load UI settings between application sessions. + +* *Advanced Layout*: Use ImGui's docking features to create more complex UI layouts, such as dockable panels. + +* *Custom Widgets*: Develop custom widgets for specific needs in your application, such as a color wheel, a curve editor, or a node graph editor. + +* *Performance Optimization*: Profile and optimize the GUI rendering to minimize its impact on overall application performance, especially on mobile devices with limited resources. + +* *Battery Efficiency*: For mobile platforms, optimize the GUI rendering to minimize battery usage. + +=== Integration with Other Systems + +As you continue building your Vulkan engine, consider how the GUI system integrates with other components: + +* *Scene Graph*: How can the GUI be used to visualize and edit the scene graph hierarchy across different platforms? + +* *Material System*: Can you create a material editor using the GUI to adjust material properties in real-time, with interfaces that work well on both desktop and mobile? + +* *Animation System*: How might the GUI be used to control and visualize animations, with controls that are appropriate for each platform? + +* *Physics System*: Could the GUI provide tools for setting up and debugging physics simulations, with different interaction models for desktop and mobile? + +* *Device-Specific Features*: How can you leverage specific features (like haptic feedback on mobile) while maintaining a consistent core experience? + +By addressing these questions, you can create a more cohesive and powerful engine that leverages the GUI for both development and runtime functionality across multiple platforms. + +=== Cross-Platform Considerations + +When developing a GUI system that works across platforms, keep these considerations in mind: + +* *Input Methods*: Different platforms have different primary input methods (mouse/keyboard vs. touch). + +* *Screen Sizes*: Interfaces need to work on screens ranging from small phones to large monitors. + +* *Performance Constraints*: Mobile devices typically have less processing power and memory than desktops. + +* *Battery Life*: On mobile devices, efficient rendering is crucial for battery life. + +* *Platform Conventions*: Users expect applications to follow platform-specific UI conventions. + +* *Testing*: Cross-platform applications require testing on all target platforms. + +=== Alternative GUI Libraries for Vulkan + +While we've focused on https://github.com/ocornut/imgui[Dear ImGui] in this chapter, there are several other GUI libraries that work well with Vulkan. Understanding the options can help you choose the right tool for your specific needs: + +* https://github.com/Immediate-Mode-UI/Nuklear[*Nuklear*]: A minimalist immediate-mode GUI library with a small footprint. It's designed to be embedded directly into applications and supports Vulkan among other rendering backends. Nuklear is used in smaller indie games and tools due to its simplicity and low overhead. + +* https://www.qt.io/[*Qt*]: A comprehensive UI framework that added Vulkan support in Qt 5.10. Qt provides a more traditional retained-mode GUI approach with a rich set of widgets and tools. It's used in applications like the Autodesk Maya viewport and various CAD software. + +* http://cegui.org.uk/[*CEGUI*]: The Crazy Eddie's GUI system is a free library providing windowing and widgets for games and simulation applications. It has Vulkan renderer support and is used in some indie game engines. + +* https://ultralig.ht/[*Ultralight*]: A lightweight, high-performance HTML renderer designed for game and application UIs. It can be integrated with Vulkan and is used by developers who want to leverage web technologies for their interfaces. + +* https://www.noesisengine.com/[*Noesis GUI*]: A commercial UI middleware that supports XAML and can render through Vulkan. It's used in games like Dauntless and provides a designer-friendly workflow. + +Popular game engines have different approaches to GUI integration with Vulkan: + +* https://www.unrealengine.com/[*Unreal Engine*]: Uses its own proprietary Slate UI framework internally, which has been adapted to work with Vulkan when the engine uses this rendering backend. For in-game UIs, it uses https://docs.unrealengine.com/5.0/en-US/umg-ui-designer-for-unreal-engine/[Unreal Motion Graphics (UMG)], which is built on top of Slate. + +* https://unity.com/[*Unity*]: When using the Vulkan renderer, Unity's built-in UI system (https://docs.unity3d.com/Manual/UIToolkit.html[UI Toolkit] and legacy https://docs.unity3d.com/Manual/com.unity.ugui.html[UGUI]) works transparently with Vulkan. Unity abstracts the rendering details away from the UI system. + +* https://godotengine.org/[*Godot*]: Uses its own built-in UI system that works across all its rendering backends, including Vulkan. The UI nodes are integrated directly into the scene tree. + +* https://www.cryengine.com/[*CryEngine*]: Employs https://www.autodesk.com/products/scaleform/overview[Scaleform] for UI rendering, which has been adapted to work with the engine's Vulkan implementation. + +* *Custom engines*: Many AAA studios with proprietary engines either develop custom UI solutions or integrate middleware like https://coherent-labs.com/[Coherent UI] or https://www.autodesk.com/products/scaleform/overview[Scaleform], adapting them to work with their Vulkan renderers. + +When choosing a GUI library for your Vulkan application, consider factors like: + +* Development paradigm (immediate-mode vs. retained-mode) +* Performance requirements +* Designer-friendliness +* Learning curve +* Licensing and cost +* Platform support +* Integration complexity + +Dear ImGui, which we've used in this chapter, strikes a good balance for many developers due to its simplicity, performance, and ease of integration with Vulkan. + +=== Final Thoughts + +A well-designed GUI is essential for creating user-friendly applications that can reach a wide audience. It serves as the primary way users interact with your application and can significantly impact the user experience. By understanding how to integrate Dear ImGui with Vulkan and implementing a robust input handling system that works with basic inputs for mouse and keyboard, you've taken a major step toward creating professional-quality applications. + +Remember that the code provided in this chapter is a starting point. Feel free to modify and extend it to suit your specific needs and application requirements. The flexibility of our approach allows for a wide range of customization and extension while maintaining compatibility with multiple platforms. + +In the next chapter, we'll explore how to load and render 3D models, which will allow us to create more complex and visually interesting scenes. + +link:05_vulkan_integration.adoc[Previous: Vulkan Integration] | link:../Loading_Models/01_introduction.adoc[Next: Loading Models] diff --git a/en/Building_a_Simple_Engine/GUI/index.adoc b/en/Building_a_Simple_Engine/GUI/index.adoc new file mode 100644 index 00000000..f0d5e4df --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/index.adoc @@ -0,0 +1,22 @@ +::pp: {plus}{plus} + += GUI: Introduce working with a GUI and handling input +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +include::01_introduction.adoc[] + +include::02_imgui_setup.adoc[] + +include::03_input_handling.adoc[] + +include::04_ui_elements.adoc[] + +include::05_vulkan_integration.adoc[] + +include::06_conclusion.adoc[] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc new file mode 100644 index 00000000..e6461d3d --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc @@ -0,0 +1,176 @@ += Introduction to Lighting & Materials + +In this chapter, we'll explore the fundamentals of lighting and materials in 3D rendering, with a focus on Physically Based Rendering (PBR). Lighting is a crucial aspect of creating realistic and visually appealing 3D scenes. Without proper lighting, even the most detailed models can appear flat and lifeless. + +This chapter serves as the foundation for understanding how light interacts with different materials in a physically accurate way. The concepts you'll learn here will be applied in later chapters, including the Loading_Models chapter where we'll use this knowledge to render glTF models with PBR materials. + +Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== Why Lighting Matters + +Lighting in computer graphics serves several important purposes: + +1. *Visual Realism*: Proper lighting creates shadows, highlights, and gradients that make 3D objects appear more realistic. +2. *Spatial Understanding*: Lighting helps viewers understand the spatial relationships between objects in a scene. +3. *Mood and Atmosphere*: Different lighting setups can dramatically change the mood and atmosphere of a scene. +4. *Focus and Attention*: Lighting can be used to draw attention to important elements in a scene. + +== Physically Based Rendering (PBR) + +=== Introduction to PBR + +Physically Based Rendering (PBR) represents one of the most significant advancements in real-time graphics over the past decade. Unlike traditional rendering approaches that used ad-hoc shading models, PBR aims to simulate how light interacts with surfaces in the real world based on the principles of physics. + +=== The Evolution of Real-Time Rendering + +To appreciate PBR, it helps to understand how real-time rendering has evolved: + +1. *Fixed-Function Pipeline (1990s)*: Early 3D hardware used fixed lighting models like Gouraud or Phong shading with limited material properties. + +2. *Programmable Shaders (2000s)*: With the introduction of shader programming, developers could implement custom lighting models, but these were often inconsistent across different lighting conditions. + +3. *Physically Based Rendering (2010s)*: By basing rendering on physical principles, PBR provides more realistic results that remain consistent across different environments. + +The key advantages of PBR include: + +* *Realism*: Materials look correct under any lighting condition +* *Consistency*: Artists can create materials that work in all environments +* *Intuitiveness*: Material parameters have physical meaning, making them easier to understand +* *Efficiency*: Modern PBR implementations are optimized for real-time performance + +=== Core Principles of PBR + +PBR is built on several key principles that distinguish it from earlier rendering approaches: + +==== Energy Conservation + +In the real world, a surface cannot reflect more light than it receives. This principle of energy conservation is fundamental to PBR: + +* The sum of diffuse and specular reflection must not exceed 1.0 +* As surfaces become more metallic, they have less diffuse reflection +* As surfaces become rougher, specular highlights become larger but less intense + +==== Microfacet Theory + +PBR uses microfacet theory to model surface roughness. This theory assumes that surfaces are composed of tiny, perfectly reflective microfacets with varying orientations: + +* Smooth surfaces have microfacets that are mostly aligned, creating sharp reflections +* Rough surfaces have randomly oriented microfacets, scattering light and creating blurry reflections +* The distribution of these microfacets is controlled by the roughness parameter + +==== Fresnel Effect + +The Fresnel effect describes how reflectivity changes with viewing angle: + +* All surfaces become more reflective at grazing angles (angles where the viewing direction is nearly parallel to the surface) +* This effect is more noticeable on smooth surfaces +* The base reflectivity at normal incidence (F0, when light hits the surface perpendicularly), is determined by the material's index of refraction +* For metals, F0 is colored (based on the metal's properties) +* For non-metals (dielectrics), F0 is typically around 0.04 (4%) + +==== Metallic-Roughness Workflow + +The PBR implementation in glTF and many modern engines uses the metallic-roughness workflow, which defines materials using these primary parameters: + +* *Base Color*: The albedo or diffuse color of the surface +* *Metallic*: How "metal-like" the surface is (0.0 = non-metal, 1.0 = metal) +* *Roughness*: How smooth or rough the surface is (0.0 = mirror-like, 1.0 = rough) + +This workflow is intuitive for artists and efficient for real-time rendering. + +=== The BRDF in PBR + +The Bidirectional Reflectance Distribution Function (BRDF) is at the heart of PBR. It describes how light is reflected from a surface, taking into account: + +* The incoming light direction +* The outgoing view direction +* The surface normal +* The material properties + +In PBR, the BRDF is typically split into two components: + +* *Diffuse BRDF*: Handles light that penetrates the surface, scatters, and exits +* *Specular BRDF*: Handles light that reflects directly from the surface + +==== Diffuse BRDF + +The simplest diffuse BRDF is the Lambertian model: + +[source] +---- +f_diffuse = albedo / π +---- + +Where: +* albedo is the base color of the surface +* π is a normalization factor + +More advanced models like Disney's diffuse or Oren-Nayar can be used for increased realism, especially for rough surfaces. + +==== Specular BRDF + +For the specular component, PBR typically uses a microfacet BRDF: + +[source] +---- +f_specular = D * F * G / (4 * (n·ωo) * (n·ωi)) +---- + +Where: +* D is the Normal Distribution Function (NDF) +* F is the Fresnel term +* G is the Geometry term +* n is the surface normal +* ωo is the outgoing (view) direction +* ωi is the incoming (light) direction + +Popular implementations include: +* *D*: GGX (Trowbridge-Reitz) distribution +* *F*: Schlick's approximation +* *G*: Smith shadowing-masking function + +== Materials in Computer Graphics + +Materials define how surfaces interact with light. Different materials reflect, absorb, and transmit light in different ways. Understanding materials is crucial for creating realistic renderings. + +=== Material Properties + +In computer graphics, materials are defined by various properties: + +* *Base Color/Albedo*: The color of the surface under diffuse lighting +* *Metalness*: How metallic the surface is (affects specular reflection and diffuse absorption) +* *Roughness/Smoothness*: How rough or smooth the surface is (affects specular highlight size and sharpness) +* *Normal Map*: Adds surface detail without increasing geometric complexity +* *Ambient Occlusion*: Approximates how much ambient light a surface point receives +* *Emissive*: Makes parts of the surface emit light +* *Opacity/Transparency*: Controls how transparent the material is +* *Refraction*: Controls how light bends when passing through the material + +=== Common Material Types + +Different types of materials have different characteristics: + +* *Metals*: High specular reflection, colored specular, no diffuse reflection +* *Dielectrics (Non-metals)*: Lower specular reflection, white specular, strong diffuse reflection +* *Translucent Materials*: Allow light to pass through and scatter within (e.g., skin, wax, marble) +* *Transparent Materials*: Allow light to pass through with minimal scattering (e.g., glass, water) +* *Anisotropic Materials*: Reflect light differently based on direction (e.g., brushed metal, hair) + +=== Push Constants for Material Properties + +In our implementation, we'll use push constants to efficiently pass material properties to our shaders. + +Push constants are a way to send a small amount of data to shaders without having to create and manage descriptor sets. They're perfect for frequently changing data like material properties. + +== What You'll Learn + +By the end of this chapter, you'll understand: + +1. How Physically Based Rendering works +2. How to implement PBR in Slang shaders +3. How to use push constants for material properties +4. How to integrate PBR lighting with Vulkan + +Let's get started by exploring the principles of Physically Based Rendering in more detail. + +link:../Camera_Transformations/06_conclusion.adoc[Previous: Camera Transformations - Conclusion] | link:02_lighting_models.adoc[Next: Lighting Models] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc new file mode 100644 index 00000000..ecc3e2a2 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc @@ -0,0 +1,187 @@ += Lighting Models + +In this section, we'll explore various lighting models used in computer graphics, with a focus on understanding the concepts rather than implementation details. We'll discuss how different lighting models simulate the interaction of light with surfaces, their advantages and limitations, and when to use each approach. + +In this chapter, we'll introduce Physically Based Rendering (PBR) and other lighting models. The concepts we cover here will be applied in later chapters, such as the Loading_Models chapter where we'll use glTF, which uses PBR with the metallic-roughness workflow for its material system. By understanding the theory behind different lighting models, including PBR, we can better leverage the material properties provided by glTF models and extend our rendering capabilities. + +Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== Understanding Light-Surface Interaction + +Before diving into specific lighting models, it's important to understand how light interacts with surfaces in the real world: + +* *Reflection*: Light bounces off the surface +* *Absorption*: Light is absorbed by the surface and converted to heat +* *Transmission*: Light passes through the surface (for transparent materials) +* *Scattering*: Light is scattered in various directions within the material + +The way these interactions occur depends on the material properties and the characteristics of the light. + +=== Types of Reflection + +There are two main types of reflection: + +* *Diffuse Reflection*: Light is scattered in many directions, creating a matte appearance +* *Specular Reflection*: Light is reflected in a specific direction, creating highlights + +Most real-world materials exhibit a combination of diffuse and specular reflection. + +== The Evolution of Lighting Models + +Lighting models in computer graphics have evolved significantly over time, each with their own approach to simulating light-surface interactions: + +=== Early Lighting Models + +==== Flat Shading + +The simplest lighting model, where each polygon is assigned a single color based on its normal and the light direction. This creates a faceted appearance with visible polygon edges. + +* *Advantages*: Very fast to compute +* *Disadvantages*: Unrealistic appearance, visible polygon edges +* *When to use*: For very low-power devices or stylized rendering + +==== Gouraud Shading + +An improvement over flat shading, where lighting is calculated at the vertices and then interpolated across the polygon. + +* *Advantages*: Smoother appearance than flat shading, still relatively fast +* *Disadvantages*: Cannot accurately represent specular highlights +* *When to use*: For low-power devices where Phong shading is too expensive + +==== Phong Lighting Model + +One of the most widely used traditional lighting models, developed by Bui Tuong Phong in 1975. It calculates lighting using three components: + +* *Ambient*: A constant light level to simulate indirect lighting +* *Diffuse*: Light scattered in all directions (using Lambert's cosine law) +* *Specular*: Shiny highlights (using a power function of the reflection vector and view vector) + +* *Advantages*: Reasonably realistic for many materials, intuitive parameters +* *Disadvantages*: Not physically accurate, can look artificial under certain lighting conditions +* *When to use*: For simple real-time applications where PBR is too expensive + +For more information on the Phong lighting model, see the link:https://en.wikipedia.org/wiki/Phong_reflection_model[Wikipedia article] or this link:https://www.cs.utah.edu/~shirley/books/fcg2/rt.pdf[computer graphics textbook]. + +==== Blinn-Phong Model + +A modification of the Phong model by Jim Blinn that uses the halfway vector between the light and view directions instead of the reflection vector, making it more efficient to compute. + +* *Advantages*: Faster than Phong, similar visual results +* *Disadvantages*: Still not physically accurate +* *When to use*: As a more efficient alternative to Phong + +Learn more about Blinn-Phong in this link:https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model[Wikipedia article] or this link:https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-5-implementing-improved-perlin-noise[GPU Gems chapter]. + +=== Advanced Lighting Models + +==== Cook-Torrance Model + +A more physically-based model developed by Robert Cook and Kenneth Torrance in 1982. It uses microfacet theory to model surface roughness and includes a more accurate specular term. + +* *Advantages*: More physically accurate than Phong or Blinn-Phong +* *Disadvantages*: More complex to implement and compute +* *When to use*: When you need more realistic materials but full PBR is too expensive + +For more details, see the original link:https://graphics.pixar.com/library/ReflectanceModel/paper.pdf[Cook-Torrance paper]. + +==== Oren-Nayar Model + +An extension of the Lambertian diffuse model that accounts for microfacet roughness in diffuse reflection, making it more suitable for rough surfaces like cloth, concrete, or sand. + +* *Advantages*: More realistic diffuse reflection for rough surfaces +* *Disadvantages*: More expensive than Lambertian diffuse +* *When to use*: For materials where diffuse roughness is important + +Learn more in the original link:https://www1.cs.columbia.edu/CAVE/publications/pdfs/Oren_SIGGRAPH94.pdf[Oren-Nayar paper]. + +==== Physically Based Rendering (PBR) + +PBR represents one of the most significant advancements in real-time graphics over the past decade. Unlike earlier ad-hoc shading models, PBR aims to simulate how light interacts with surfaces based on the principles of physics. + +The key principles of PBR include: + +* *Energy Conservation*: A surface cannot reflect more light than it receives +* *Microfacet Theory*: Surfaces are modeled as collections of tiny mirrors with varying orientations +* *Fresnel Effect*: Reflectivity changes with viewing angle +* *Metallic-Roughness Workflow*: Materials are defined by their base color, metalness, and roughness + +* *Advantages*: Realistic results that remain consistent across different lighting conditions, intuitive parameters for artists +* *Disadvantages*: More complex and computationally expensive +* *When to use*: For modern games and applications where realism is important + +For comprehensive information on PBR, see the link:https://www.pbr-book.org/[Physically Based Rendering book]. + +== Lighting Models in glTF + +The glTF format uses PBR with the metallic-roughness workflow, which defines materials using these primary parameters: + +* *Base Color*: The albedo or diffuse color of the surface +* *Metallic*: How "metal-like" the surface is (0.0 = non-metal, 1.0 = metal) +* *Roughness*: How smooth or rough the surface is (0.0 = mirror-like, 1.0 = rough) + +This workflow is intuitive for artists and efficient for real-time rendering. The glTF specification provides a standardized way to define PBR materials that can be used across different rendering engines. + +For more information on the glTF PBR implementation, see the link:https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials[glTF 2.0 specification]. + +== Light Types + +Different lighting models can work with various types of light sources: + +1. *Point Lights*: Light emanates in all directions from a single point. +2. *Directional Lights*: Light rays are parallel, as if coming from a very distant source (like the sun). +3. *Spot Lights*: Light is emitted in a cone shape from a point. +4. *Area Lights*: Light is emitted from a surface area. +5. *Image-Based Lighting (IBL)*: Light is derived from an environment map, simulating global illumination. + +Each type of light requires specific calculations for the light direction, attenuation, and other properties. + +== Advanced Lighting Techniques + +Beyond basic lighting models, there are several advanced techniques that can enhance the realism of your rendering: + +=== Global Illumination + +Global Illumination (GI) simulates how light bounces between surfaces, creating indirect lighting effects. Techniques include: + +* *Radiosity*: Calculates diffuse light transfer between surfaces +* *Path Tracing*: Traces light paths through the scene +* *Photon Mapping*: Stores light information in a spatial data structure + +For more information, see this link:https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-12-tricks-real-time-radiosity[GPU Gems chapter on radiosity]. + +=== Subsurface Scattering + +Subsurface Scattering (SSS) simulates how light penetrates and scatters within translucent materials like skin, wax, or marble. + +For more information, see this link:https://developer.nvidia.com/gpugems/gpugems/part-iii-materials/chapter-16-real-time-approximations-subsurface-scattering[GPU Gems chapter on subsurface scattering]. + +=== Ambient Occlusion + +Ambient Occlusion (AO) approximates how much ambient light a surface point would receive, darkening corners and crevices. + +For more information, see this link:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-17-ambient-occlusion[GPU Gems chapter on ambient occlusion]. + +== Choosing the Right Lighting Model + +When deciding which lighting model to use for your application, consider: + +1. *Hardware Constraints*: More complex models require more processing power +2. *Visual Requirements*: How realistic do your materials need to look? +3. *Artist Workflow*: Some models are more intuitive for artists to work with +4. *Consistency*: PBR provides more consistent results across different lighting conditions + +For our engine, we'll leverage the PBR implementation from the glTF format, as it provides a good balance of realism, performance, and artist-friendly parameters. + +== Further Reading + +To deepen your understanding of lighting models, here are some valuable resources: + +* link:https://www.pbr-book.org/[Physically Based Rendering: From Theory to Implementation] - The definitive book on PBR +* link:https://learnopengl.com/PBR/Theory[LearnOpenGL PBR Tutorial] - An accessible introduction to PBR concepts +* link:https://google.github.io/filament/Filament.html[Filament Material System] - Google's real-time PBR rendering engine documentation +* link:https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials[glTF 2.0 Material Specification] - Details on how PBR is implemented in glTF +* link:https://developer.nvidia.com/gpugems/gpugems/part-iii-materials[GPU Gems: Materials] - Collection of articles on advanced material rendering + +In the next section, we'll explore how to use push constants to efficiently pass material properties to our shaders. + +link:01_introduction.adoc[Previous: Introduction] | link:03_push_constants.adoc[Next: Push Constants] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc new file mode 100644 index 00000000..beeb3286 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc @@ -0,0 +1,143 @@ += Push Constants + +In this section, we'll explore push constants, a powerful feature in Vulkan that allows us to efficiently pass small amounts of data to shaders without the overhead of descriptor sets. + +Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== What Are Push Constants? + +Push constants are a way to send a small amount of data directly to shaders. Unlike uniform buffers, which require descriptor sets and memory allocation, push constants are part of the command buffer itself. This makes them ideal for small, frequently changing data. + +Some key characteristics of push constants: + +1. *Size Limitations*: Push constants have a limited size (typically 128 bytes, but this can vary depending on the device). +2. *Efficiency*: They're more efficient than uniform buffers for small, frequently changing data. +3. *Simplicity*: They don't require descriptor sets or memory allocation. +4. *Scope*: They're available to all shader stages in a pipeline. + +== When to Use Push Constants + +Push constants are particularly useful for: + +1. *Per-Draw Data*: Data that changes for each draw call, such as material properties. +2. *Small Data Sets*: Data that fits within the size limitations of push constants. +3. *Frequently Changing Data*: Data that changes often, where the overhead of updating a uniform buffer would be significant. + +For our PBR implementation, push constants are perfect for storing material properties like base color, metallic factor, and roughness factor. + +== Defining Push Constants in Shaders + +In GLSL (or SPIR-V), push constants are defined using a uniform block with the `push_constant` layout qualifier: + +[source,glsl] +---- +layout(push_constant) uniform PushConstants { + vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +} material; +---- + +In Slang, which we're using for our engine, the syntax is slightly different: + +[source,slang] +---- +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +}; + +[[vk::push_constant]] PushConstants material; +---- + +== Setting Up Push Constants in Vulkan + +To use push constants in Vulkan with vk::raii, we need to: + +1. Define a push constant range when creating the pipeline layout. +2. Use `commandBuffer.pushConstants` to send data to the shader. + +Here's how we define a push constant range: + +[source,cpp] +---- +// Set up push constant range for material properties +vk::PushConstantRange pushConstantRange; +pushConstantRange.setStageFlags(vk::ShaderStageFlagBits::eFragment) // Which shader stages can access the push constants + .setOffset(0) + .setSize(sizeof(PushConstantBlock)); // Size of our push constant data + +// Create pipeline layout with push constants +vk::PipelineLayoutCreateInfo pipelineLayoutInfo; +pipelineLayoutInfo.setSetLayoutCount(1) + .setPSetLayouts(&*descriptorSetLayout) + .setPushConstantRangeCount(1) + .setPPushConstantRanges(&pushConstantRange); + +// Create pipeline layout with vk::raii +vk::raii::PipelineLayout pipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); +---- + +And here's how we send data to the shader: + +[source,cpp] +---- +// Define material properties +PushConstantBlock pushConstants{}; +pushConstants.baseColorFactor = {1.0f, 1.0f, 1.0f, 1.0f}; +pushConstants.metallicFactor = 1.0f; +pushConstants.roughnessFactor = 0.5f; +pushConstants.baseColorTextureSet = 0; +pushConstants.physicalDescriptorTextureSet = 1; +pushConstants.normalTextureSet = 2; +pushConstants.occlusionTextureSet = 3; +pushConstants.emissiveTextureSet = 4; +pushConstants.alphaMask = 0.0f; +pushConstants.alphaMaskCutoff = 0.5f; + +// Push constants to shader using vk::raii +commandBuffer.pushConstants( + *pipelineLayout, + vk::ShaderStageFlagBits::eFragment, // Which shader stages will receive the data + 0, // Offset + sizeof(PushConstantBlock), // Size + &pushConstants // Data +); +---- + +== Push Constants vs. Uniform Buffers + +While push constants are efficient for small, frequently changing data, they have limitations. For larger data sets or data that doesn't change frequently, uniform buffers are often a better choice. + +Here's a comparison: + +|=== +| Feature | Push Constants | Uniform Buffers +| Size | Limited (typically 128 bytes) | Much larger +| Update Mechanism | Direct command in command buffer | Memory mapping or staging buffer +| Descriptor Sets | Not required | Required +| Memory Allocation | Not required | Required +| Update Frequency | Ideal for frequent updates | Better for infrequent updates +| Access Speed | Fast | Slightly slower +|=== + +For our PBR implementation, we'll use push constants for material properties and uniform buffers for light information and transformation matrices. + +In the next section, we'll implement a basic lighting shader that uses push constants for material properties. + +link:02_lighting_models.adoc[Previous: Lighting Models] | link:04_lighting_implementation.adoc[Next: Lighting Implementation] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc new file mode 100644 index 00000000..8a258858 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc @@ -0,0 +1,509 @@ += PBR Lighting Implementation + +In this section, we'll implement a Physically Based Rendering (PBR) shader based on the concepts we've explored in the previous sections. This shader will use the metallic-roughness workflow that's compatible with glTF models and push constants for material properties. We'll examine the shader implementation and then discuss how to integrate it with our engine. + +Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== Implementing the PBR Shader + +Let's create a PBR shader, which we'll name `pbr.slang`. This shader implements the metallic-roughness workflow that we've discussed, making it compatible with glTF models. It uses push constants for material properties and uniform buffers for transformation matrices and light information. + +[source,slang] +---- +// Combined vertex and fragment shader for PBR rendering + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos : POSITION; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPositions[4]; + float4 lightColors[4]; + float4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +}; + +// Push constants for material properties +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +}; + +// Constants +static const float PI = 3.14159265359; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Texture2D baseColorMap; +[[vk::binding(1, 0)]] SamplerState baseColorSampler; +[[vk::binding(2, 0)]] Texture2D metallicRoughnessMap; +[[vk::binding(2, 0)]] SamplerState metallicRoughnessSampler; +[[vk::binding(3, 0)]] Texture2D normalMap; +[[vk::binding(3, 0)]] SamplerState normalSampler; +[[vk::binding(4, 0)]] Texture2D occlusionMap; +[[vk::binding(4, 0)]] SamplerState occlusionSampler; +[[vk::binding(5, 0)]] Texture2D emissiveMap; +[[vk::binding(5, 0)]] SamplerState emissiveSampler; + +[[vk::push_constant]] PushConstants material; + +// PBR functions +float DistributionGGX(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH2 = NdotH * NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + + return ggx1 * ggx2; +} + +float3 FresnelSchlick(float cosTheta, float3 F0) { + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal to world space + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass texture coordinates + output.UV = input.UV; + + // Pass tangent + output.Tangent = input.Tangent; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample material textures + float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float metallic = metallicRoughness.x * material.metallicFactor; + float roughness = metallicRoughness.y * material.roughnessFactor; + float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; + float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; + + // Calculate normal in tangent space + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + // Apply normal mapping + float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + float3 T = normalize(input.Tangent.xyz); + float3 B = normalize(cross(N, T)) * input.Tangent.w; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(tangentNormal, TBN)); + } + + // Calculate view and reflection vectors + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + float3 R = reflect(-V, N); + + // Calculate F0 (base reflectivity) + float3 F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + + // Initialize lighting + float3 Lo = float3(0.0, 0.0, 0.0); + + // Calculate lighting for each light + for (int i = 0; i < 4; i++) { + float3 lightPos = ubo.lightPositions[i].xyz; + float3 lightColor = ubo.lightColors[i].rgb; + + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + // Calculate half vector + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // Energy conservation + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Add to outgoing radiance + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + + // Add ambient and emissive + float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + float3 color = ambient + Lo + emissive; + + // HDR tonemapping and gamma correction + color = color / (color + float3(1.0, 1.0, 1.0)); + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + return float4(color, baseColor.a); +} +---- + +This shader implements the PBR lighting model with the metallic-roughness workflow. It includes: + +1. *Normal Distribution Function (D)*: Using the GGX (Trowbridge-Reitz) distribution +2. *Geometry Function (G)*: Using the Smith shadowing-masking function +3. *Fresnel Term (F)*: Using Schlick's approximation +4. *Energy Conservation*: Ensuring that diffuse and specular reflection don't exceed the incoming light +5. *Normal Mapping*: For adding surface detail without increasing geometric complexity +6. *HDR Tonemapping*: For handling high dynamic range lighting +7. *Gamma Correction*: For proper color representation + +== Extending the Renderer + +Now that we have our PBR shader, we need to extend our renderer to support it. We'll need to: + +1. Add a new pipeline for our PBR shader +2. Add support for push constants +3. Update the uniform buffer to include light information + +Let's start by adding a new function to create the PBR pipeline: + +[source,cpp] +---- +bool Renderer::createPBRPipeline() { + try { + // Load combined PBR shader + auto shaderCode = readFile("shaders/pbr.spv"); + + // Create shader module with vk::raii + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Set up shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo; + vertShaderStageInfo.setStage(vk::ShaderStageFlagBits::eVertex) + .setModule(*shaderModule) + .setPName("VSMain"); // Entry point for vertex shader + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo; + fragShaderStageInfo.setStage(vk::ShaderStageFlagBits::eFragment) + .setModule(*shaderModule) + .setPName("PSMain"); // Entry point for fragment shader + + std::array shaderStages = {vertShaderStageInfo, fragShaderStageInfo}; + + // Vertex input state + vk::PipelineVertexInputStateCreateInfo vertexInputInfo; + + // Define vertex binding and attributes for PBR + vk::VertexInputBindingDescription bindingDescription; + bindingDescription.setBinding(0) + .setStride(sizeof(float) * 14) // pos(3) + normal(3) + texCoord(2) + tangent(4) + bitangent(2) + .setInputRate(vk::VertexInputRate::eVertex); + + std::array attributeDescriptions; + // Position + attributeDescriptions[0].setBinding(0) + .setLocation(0) + .setFormat(vk::Format::eR32G32B32Sfloat) + .setOffset(0); + // Normal + attributeDescriptions[1].setBinding(0) + .setLocation(1) + .setFormat(vk::Format::eR32G32B32Sfloat) + .setOffset(sizeof(float) * 3); + // Texture coordinates + attributeDescriptions[2].setBinding(0) + .setLocation(2) + .setFormat(vk::Format::eR32G32Sfloat) + .setOffset(sizeof(float) * 6); + // Tangent + attributeDescriptions[3].setBinding(0) + .setLocation(3) + .setFormat(vk::Format::eR32G32B32A32Sfloat) + .setOffset(sizeof(float) * 8); + // Bitangent + attributeDescriptions[4].setBinding(0) + .setLocation(4) + .setFormat(vk::Format::eR32G32Sfloat) + .setOffset(sizeof(float) * 12); + + vertexInputInfo.setVertexBindingDescriptionCount(1) + .setPVertexBindingDescriptions(&bindingDescription) + .setVertexAttributeDescriptionCount(static_cast(attributeDescriptions.size())) + .setPVertexAttributeDescriptions(attributeDescriptions.data()); + + // Input assembly state + vk::PipelineInputAssemblyStateCreateInfo inputAssembly; + inputAssembly.setTopology(vk::PrimitiveTopology::eTriangleList) + .setPrimitiveRestartEnable(false); + + // Viewport and scissor state + vk::PipelineViewportStateCreateInfo viewportState; + viewportState.setViewportCount(1) + .setScissorCount(1); + + // Dynamic state for viewport and scissor + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState; + dynamicState.setDynamicStateCount(static_cast(dynamicStates.size())) + .setPDynamicStates(dynamicStates.data()); + + // Rasterization state + vk::PipelineRasterizationStateCreateInfo rasterizer; + rasterizer.setDepthClampEnable(false) + .setRasterizerDiscardEnable(false) + .setPolygonMode(vk::PolygonMode::eFill) + .setLineWidth(1.0f) + .setCullMode(vk::CullModeFlagBits::eBack) + .setFrontFace(vk::FrontFace::eCounterClockwise) + .setDepthBiasEnable(false); + + // Multisample state + vk::PipelineMultisampleStateCreateInfo multisampling; + multisampling.setSampleShadingEnable(false) + .setRasterizationSamples(vk::SampleCountFlagBits::e1); + + // Depth and stencil state + vk::PipelineDepthStencilStateCreateInfo depthStencil; + depthStencil.setDepthTestEnable(true) + .setDepthWriteEnable(true) + .setDepthCompareOp(vk::CompareOp::eLess) + .setDepthBoundsTestEnable(false) + .setStencilTestEnable(false); + + // Color blend state + vk::PipelineColorBlendAttachmentState colorBlendAttachment; + colorBlendAttachment.setColorWriteMask( + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA) + .setBlendEnable(true) + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + + vk::PipelineColorBlendStateCreateInfo colorBlending; + colorBlending.setLogicOpEnable(false) + .setAttachmentCount(1) + .setPAttachments(&colorBlendAttachment); + + // Set up push constant range for material properties + vk::PushConstantRange pushConstantRange; + pushConstantRange.setStageFlags(vk::ShaderStageFlagBits::eFragment) + .setOffset(0) + .setSize(sizeof(PushConstantBlock)); // Size of our push constant data + + // Create pipeline layout with push constants + vk::PipelineLayoutCreateInfo pipelineLayoutInfo; + pipelineLayoutInfo.setSetLayoutCount(1) + .setPSetLayouts(&*descriptorSetLayout) + .setPushConstantRangeCount(1) + .setPPushConstantRanges(&pushConstantRange); + + // Create pipeline layout with vk::raii + pbrPipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); + + // Create the PBR graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo; + pipelineInfo.setStageCount(static_cast(shaderStages.size())) + .setPStages(shaderStages.data()) + .setPVertexInputState(&vertexInputInfo) + .setPInputAssemblyState(&inputAssembly) + .setPViewportState(&viewportState) + .setPRasterizationState(&rasterizer) + .setPMultisampleState(&multisampling) + .setPDepthStencilState(&depthStencil) + .setPColorBlendState(&colorBlending) + .setPDynamicState(&dynamicState) + .setLayout(*pbrPipelineLayout) + .setRenderPass(nullptr) // Using dynamic rendering + .setSubpass(0) + .setBasePipelineHandle(nullptr); + + // Set up dynamic rendering info + vk::PipelineRenderingCreateInfo renderingInfo; + renderingInfo.setColorAttachmentCount(1) + .setPColorAttachmentFormats(&swapChainImageFormat) + .setDepthAttachmentFormat(findDepthFormat()); + pipelineInfo.setPNext(&renderingInfo); + + // Create graphics pipeline with vk::raii + pbrPipeline = device.createGraphicsPipeline(nullptr, pipelineInfo); + + // With vk::raii, shader module is automatically destroyed when it goes out of scope + + return true; + } catch (const std::exception& e) { + std::cerr << "Error creating PBR pipeline: " << e.what() << std::endl; + return false; + } +} +---- + +This function creates a new pipeline for our PBR shader, including support for push constants. We'll also need to update our uniform buffer to include light information: + +[source,cpp] +---- +// Update uniform buffer +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera) { + // Get the transform component from the entity + auto transform = entity->GetComponent(); + if (!transform) { + std::cerr << "Entity does not have a transform component" << std::endl; + return; + } + + // Create the uniform buffer object + UniformBufferObject ubo{}; + + // Set the model matrix from the entity's transform + ubo.model = transform->GetModelMatrix(); + + // Set the view and projection matrices from the camera + if (camera) { + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + } else { + // Default view and projection matrices if no camera is provided + ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float)swapChainExtent.height, 0.1f, 100.0f); + ubo.proj[1][1] *= -1; // Flip Y coordinate for Vulkan + } + + // Set up lights + // Light 1: White light from above + ubo.lightPositions[0] = glm::vec4(0.0f, 5.0f, 5.0f, 1.0f); + ubo.lightColors[0] = glm::vec4(300.0f, 300.0f, 300.0f, 1.0f); + + // Light 2: Blue light from the left + ubo.lightPositions[1] = glm::vec4(-5.0f, 0.0f, 0.0f, 1.0f); + ubo.lightColors[1] = glm::vec4(0.0f, 0.0f, 300.0f, 1.0f); + + // Light 3: Red light from the right + ubo.lightPositions[2] = glm::vec4(5.0f, 0.0f, 0.0f, 1.0f); + ubo.lightColors[2] = glm::vec4(300.0f, 0.0f, 0.0f, 1.0f); + + // Light 4: Green light from behind + ubo.lightPositions[3] = glm::vec4(0.0f, -5.0f, 0.0f, 1.0f); + ubo.lightColors[3] = glm::vec4(0.0f, 300.0f, 0.0f, 1.0f); + + // Set camera position for view-dependent effects + ubo.camPos = glm::vec4(camera ? camera->GetPosition() : glm::vec3(2.0f, 2.0f, 2.0f), 1.0f); + + // Set PBR parameters + ubo.exposure = 4.5f; + ubo.gamma = 2.2f; + ubo.prefilteredCubeMipLevels = 1.0f; + ubo.scaleIBLAmbient = 1.0f; + + // Copy the uniform buffer object to the device memory using vk::raii + // With vk::raii, we can use the mapped memory directly + memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +} +---- + +Finally, we need to add support for pushing material properties to the shader: + +[source,cpp] +---- +// Push material properties to shader +void Renderer::pushMaterialProperties(vk::CommandBuffer commandBuffer, const Model* model, uint32_t materialIndex) { + // Get material from the model + const Material& material = model->materials[materialIndex]; + + // Define push constants + PushConstantBlock pushConstants{}; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex; + pushConstants.normalTextureSet = material.normalTextureIndex; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex; + pushConstants.alphaMask = material.alphaMode == AlphaMode::MASK ? 1.0f : 0.0f; + pushConstants.alphaMaskCutoff = material.alphaCutoff; + + // Push constants to shader using vk::raii + commandBuffer.pushConstants( + *pbrPipelineLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + sizeof(PushConstantBlock), + &pushConstants + ); +} +---- + +In the next section, we'll integrate our lighting implementation with the rest of the Vulkan rendering pipeline. + +link:03_push_constants.adoc[Previous: Push Constants] | link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc new file mode 100644 index 00000000..7ddb1037 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc @@ -0,0 +1,421 @@ += Vulkan Integration + +In this section, we'll integrate our PBR implementation with the rest of the Vulkan rendering pipeline. We'll update our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. The techniques we develop here will be applied in the Loading_Models chapter when we load and render glTF models. + +Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== Updating the Renderer Class + +First, let's update our renderer class to include the new members we need for our PBR implementation: + +[source,cpp] +---- +class Renderer { +public: + // ... existing members ... + + // PBR pipeline + vk::raii::PipelineLayout pbrPipelineLayout; + vk::raii::Pipeline pbrPipeline; + + // Push constant block for PBR material properties + struct PushConstantBlock { + glm::vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; + }; + + // ... existing methods ... + + // New methods + bool createPBRPipeline(); + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const Model* model, uint32_t materialIndex); +}; +---- + +We've added members for the PBR pipeline and a struct for PBR material properties. We've also added methods for creating the PBR pipeline and pushing material properties to the shader. + +== Updating the Initialization + +Next, we need to update the initialization process to create our PBR pipeline: + +[source,cpp] +---- +bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) { + // ... existing initialization code ... + + // Create graphics pipeline + if (!createGraphicsPipeline()) { + return false; + } + + // Create PBR pipeline + if (!createPBRPipeline()) { + std::cerr << "Failed to create PBR pipeline" << std::endl; + return false; + } + + // ... rest of initialization code ... + + initialized = true; + return true; +} +---- + +== Updating the Cleanup + +We also need to update the cleanup process to destroy our PBR pipeline: + +[source,cpp] +---- +void Renderer::Cleanup() { + // ... existing cleanup code ... + + // With vk::raii, pipeline and pipeline layout objects are automatically destroyed + // when they go out of scope, so we don't need explicit destruction calls + + // ... rest of cleanup code ... +} +---- + +== Updating the Rendering Process + +Finally, we need to update the rendering process to use our PBR pipeline and push material properties: + +[source,cpp] +---- +void Renderer::recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t imageIndex) { + // ... existing command buffer recording code ... + + // Bind the PBR pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrPipeline); + + // For each model in the scene + for (const auto& model : models) { + // Bind vertex and index buffers + vk::Buffer vertexBuffers[] = {model->vertexBuffer}; + vk::DeviceSize offsets[] = {0}; + commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(model->indexBuffer, 0, vk::IndexType::eUint32); + + // For each mesh in the model + for (const auto& mesh : model->meshes) { + // Push material properties + pushMaterialProperties(commandBuffer, model, mesh.materialIndex); + + // Bind descriptor sets + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + *pbrPipelineLayout, + 0, + 1, + &descriptorSets[imageIndex], + 0, + nullptr + ); + + // Draw + commandBuffer.drawIndexed(mesh.indexCount, 1, mesh.firstIndex, 0, 0); + } + } + + // ... rest of command buffer recording code ... +} +---- + +== Creating the PBR Shader + +Now that we've updated our renderer to support our PBR implementation, we need to create the PBR shader that implements the concepts we've discussed in this chapter. Let's create a file called `pbr.slang` in our shaders directory: + +[source,cpp] +---- +// Let's create our PBR shader based on the concepts we've learned +// We can create the shader file programmatically: +// +// std::ofstream shaderFile("shaders/pbr.slang"); +// shaderFile << R"( +// +// Or we can create it manually in our shaders directory. +// +// Here's what our PBR shader looks like: +// +// Combined vertex and fragment shader for PBR rendering + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos : POSITION; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPositions[4]; + float4 lightColors[4]; + float4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +}; + +// Push constants for material properties +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +}; + +// Constants +static const float PI = 3.14159265359; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Texture2D baseColorMap; +[[vk::binding(1, 0)]] SamplerState baseColorSampler; +[[vk::binding(2, 0)]] Texture2D metallicRoughnessMap; +[[vk::binding(2, 0)]] SamplerState metallicRoughnessSampler; +[[vk::binding(3, 0)]] Texture2D normalMap; +[[vk::binding(3, 0)]] SamplerState normalSampler; +[[vk::binding(4, 0)]] Texture2D occlusionMap; +[[vk::binding(4, 0)]] SamplerState occlusionSampler; +[[vk::binding(5, 0)]] Texture2D emissiveMap; +[[vk::binding(5, 0)]] SamplerState emissiveSampler; + +[[vk::push_constant]] PushConstants material; + +// PBR functions +float DistributionGGX(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH2 = NdotH * NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + + return ggx1 * ggx2; +} + +float3 FresnelSchlick(float cosTheta, float3 F0) { + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal to world space + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass texture coordinates + output.UV = input.UV; + + // Pass tangent + output.Tangent = input.Tangent; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample material textures + float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float metallic = metallicRoughness.x * material.metallicFactor; + float roughness = metallicRoughness.y * material.roughnessFactor; + float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; + float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; + + // Calculate normal in tangent space + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + // Apply normal mapping + float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + float3 T = normalize(input.Tangent.xyz); + float3 B = normalize(cross(N, T)) * input.Tangent.w; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(tangentNormal, TBN)); + } + + // Calculate view and reflection vectors + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + float3 R = reflect(-V, N); + + // Calculate F0 (base reflectivity) + float3 F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + + // Initialize lighting + float3 Lo = float3(0.0, 0.0, 0.0); + + // Calculate lighting for each light + for (int i = 0; i < 4; i++) { + float3 lightPos = ubo.lightPositions[i].xyz; + float3 lightColor = ubo.lightColors[i].rgb; + + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + // Calculate half vector + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // Energy conservation + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Add to outgoing radiance + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + + // Add ambient and emissive + float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + float3 color = ambient + Lo + emissive; + + // HDR tonemapping and gamma correction + color = color / (color + float3(1.0, 1.0, 1.0)); + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + return float4(color, baseColor.a); +} +)"; +shaderFile.close(); +---- + +== Compiling the Shader + +After creating the shader file, we need to compile it using slangc. This is typically done as part of the build process, but we can also do it manually: + +[source,bash] +---- +slangc shaders/pbr.slang -target spirv -profile spirv_1_4 -emit-spirv-directly -o shaders/pbr.spv +---- + +== Testing the Implementation with glTF Models + +To test our implementation, we can use glTF models, which already have PBR materials defined that are compatible with our implementation. In the Loading_Models chapter, we'll learn how to load these models, but for now, let's assume we have a way to load them. + +Here's an example of how to set up a test scene with glTF models: + +[source,cpp] +---- +void Renderer::renderTestScene() { + // Set up camera + glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); + glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f); + glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); + + // Set up lights + // Light 1: White light from above + glm::vec4 lightPos1 = glm::vec4(0.0f, 5.0f, 5.0f, 1.0f); + glm::vec4 lightColor1 = glm::vec4(300.0f, 300.0f, 300.0f, 1.0f); + + // Light 2: Blue light from the left + glm::vec4 lightPos2 = glm::vec4(-5.0f, 0.0f, 0.0f, 1.0f); + glm::vec4 lightColor2 = glm::vec4(0.0f, 0.0f, 300.0f, 1.0f); + + // Load glTF models + Model* damagedHelmet = modelLoader.loadModel("models/DamagedHelmet/DamagedHelmet.gltf"); + Model* flightHelmet = modelLoader.loadModel("models/FlightHelmet/FlightHelmet.gltf"); + + // The models already have PBR materials defined in the glTF file + // We can render them directly with our PBR pipeline + + // Render the models with different transformations + renderModel(damagedHelmet, glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.5f)); + renderModel(flightHelmet, glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.5f)); + + // We can also experiment with modifying the material properties + // For example, to make the damaged helmet more metallic: + if (damagedHelmet->materials.size() > 0) { + // Store the original value to restore later + float originalMetallic = damagedHelmet->materials[0].metallicFactor; + + // Modify the material + damagedHelmet->materials[0].metallicFactor = 1.0f; + + // Render with modified material + renderModel(damagedHelmet, glm::vec3(-2.0f, 0.0f, 0.0f), glm::vec3(0.5f)); + + // Restore original value + damagedHelmet->materials[0].metallicFactor = originalMetallic; + } +} +---- + +== Conclusion + +In this section, we've integrated our PBR implementation with the rest of the Vulkan rendering pipeline. We've updated our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. We've created a PBR shader based on the concepts we've learned and shown how to test the implementation with glTF models. + +This approach provides a solid foundation for rendering physically accurate materials, which we'll apply in the Loading_Models chapter when we load and render glTF models. It also gives us the flexibility to modify and extend the material properties as needed for our specific rendering requirements. + +In the next section, we'll wrap up this chapter with a conclusion and discuss potential improvements and extensions to our lighting system. + +link:04_lighting_implementation.adoc[Previous: Lighting Implementation] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc new file mode 100644 index 00000000..cf8f0657 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc @@ -0,0 +1,51 @@ += Conclusion + +In this chapter, we've explored the fundamentals of lighting and materials in 3D rendering and introduced Physically Based Rendering (PBR) using the metallic-roughness workflow. We've covered the theory behind PBR and implemented a shader that can be used with glTF models. We've also learned how to use push constants to efficiently pass material properties to our shaders. + +Throughout our engine implementation, we've used vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== What We've Learned + +Throughout this chapter, we've covered: + +1. *Physically Based Rendering*: We've deepened our understanding of the core principles of PBR, including energy conservation, microfacet theory, and the Fresnel effect, and how they combine to create physically accurate lighting. + +2. *glTF Material Integration*: We've learned how to leverage the PBR materials defined in glTF files, taking advantage of the metallic-roughness workflow that glTF uses. + +3. *Push Constants*: We've explored push constants, a powerful feature in Vulkan that allows us to efficiently pass material properties to shaders without the overhead of descriptor sets. + +4. *Advanced Shader Techniques*: We've examined and extended the PBR shader implementation, understanding how each component contributes to realistic rendering. + +5. *Vulkan Integration*: We've integrated our enhanced PBR understanding with the rest of the Vulkan rendering pipeline, updating our renderer class to work seamlessly with glTF models and their PBR materials. + +== Potential Improvements + +While our PBR implementation is functional, there are several ways it could be improved: + +1. *Image-Based Lighting (IBL)*: Adding environment maps for ambient lighting would greatly enhance the realism of our scenes, especially for reflective materials. + +2. *Shadow Mapping*: Adding shadow mapping would greatly enhance the realism of our scenes. + +3. *Advanced Material Features*: Implementing features like clear coat, anisotropy, and subsurface scattering would allow for a wider range of material types. + +4. *Optimizations*: Implementing techniques like deferred rendering or clustered forward rendering would improve performance with many light sources. + +5. *HDR Pipeline*: Expanding our HDR pipeline with features like bloom, lens flares, and more sophisticated tone mapping operators. + +== Next Steps + +Now that you have a solid understanding of PBR and materials, you might want to explore: + +1. *Advanced Lighting Techniques*: Research more advanced lighting techniques like global illumination, ambient occlusion, and volumetric lighting. + +2. *Material Systems*: Develop a more sophisticated material system that can handle a wider range of material properties and effects. + +3. *Shader Effects*: Experiment with different shader effects like fog, bloom, and depth of field. + +4. *Performance Optimization*: Optimize your lighting calculations for better performance, especially for mobile devices or scenes with many light sources. + +Remember that lighting is a complex topic with many approaches and techniques. The implementation we've covered in this chapter is just the beginning. As you continue to develop your engine, you'll likely want to refine and expand your lighting system to meet the specific needs of your projects. + +In the next chapter, we'll explore GUI implementation, which will allow us to create interactive user interfaces for our applications. + +link:05_vulkan_integration.adoc[Previous: Vulkan Integration] | link:../GUI/01_introduction.adoc[Next: GUI - Introduction] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc new file mode 100644 index 00000000..efc0c932 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc @@ -0,0 +1,21 @@ +::pp: {plus}{plus} + += Lighting & Materials: Basic lighting models and push constants +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +This chapter covers the implementation of basic lighting models and the use of push constants for material properties in Vulkan. Throughout our engine implementation, we use vk::raii dynamic rendering and C++20 modules to create a modern, efficient, and maintainable codebase. + +== Contents + +* link:01_introduction.adoc[Introduction] +* link:02_lighting_models.adoc[Lighting Models] +* link:03_push_constants.adoc[Push Constants] +* link:04_lighting_implementation.adoc[Lighting Implementation] +* link:05_vulkan_integration.adoc[Vulkan Integration] +* link:06_conclusion.adoc[Conclusion] diff --git a/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc new file mode 100644 index 00000000..2906f138 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc @@ -0,0 +1,40 @@ +::pp: {plus}{plus} + += Loading Models: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction + +Welcome to the "Loading Models" chapter of our "Building a Simple Engine" series! After exploring engine architecture and camera systems in the previous chapters, we're now ready to focus on handling 3D assets within our engine framework. + +In this chapter, we'll set up a robust model loading system that can handle modern 3D assets. Building upon the engine architecture we've established and the camera system we've implemented, we'll now add the ability to load and render complex 3D models. In the link:../../15_GLTF_KTX2_Migration.html[chapter on glTF and KTX2] from the main tutorial, we learned about migrating from OBJ to glTF format and the basics of loading glTF models. Now, we'll integrate that knowledge into our engine structure to create a more complete implementation. + +In this chapter, we'll focus on: + +* Building a scene graph to organize 3D objects hierarchically +* Implementing animation support for glTF models +* Creating a PBR material system +* Rendering multiple objects with different transformations +* Structuring our code in a more engine-like way + +This approach will serve as the foundation for our engine and allow us to create more complex scenes with animated models. + +== Prerequisites + +Before starting this chapter, you should have completed the main Vulkan tutorial up to at least Chapter 16 (Multiple Objects). You should also be familiar with: + +* Basic Vulkan concepts: +** link:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** link:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* link:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and link:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* link:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* link:../../06_Texture_mapping/00_Images.adoc[Texture mapping] +* Basic 3D math (matrices, vectors, quaternions) - See link:../../Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc[Camera Transformations] for a refresher on 3D math. + +link:../GUI/06_conclusion.adoc[Previous: GUI] | link:02_project_setup.adoc[Next: Setting Up the Project] diff --git a/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc b/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc new file mode 100644 index 00000000..5e7b79b6 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc @@ -0,0 +1,357 @@ +::pp: {plus}{plus} + += Loading Models: Asset Pipeline Concepts +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Understanding Asset Pipelines + +After exploring engine architecture and camera systems, it's important to understand how 3D assets are managed in rendering engines. A well-designed asset pipeline is crucial for efficiently handling models, textures, and other resources in any production environment. + +=== Asset Organization Concepts + +When designing an asset organization system, consider these key principles: + +1. *Categorization* - Group similar assets together +2. *Hierarchy* - Use a nested structure to manage complexity +3. *Discoverability* - Make assets easy to find and reference +4. *Scalability* - Design for growth as your project expands + +Here's an example of how assets might be organized in a final product: + +[source] +---- +assets/ + ├── models/ // 3D model files + │ ├── characters/ // Character models + │ ├── environments/ // Environment models + │ └── props/ // Prop models + └── shaders/ // Shader files +---- + +This example demonstrates categorization (models vs. shaders) and hierarchy (character models vs. environment models). The specific organization should be tailored to your project's needs, but the underlying principles remain consistent across different engines. + +=== Asset Pipeline Concepts + +A professional asset pipeline typically involves several stages, regardless of the specific engine implementation: + +1. *Creation* - Artists create models in 3D modeling software +2. *Export* - Models are exported to interchange formats suitable for game engines +3. *Validation* - Models are checked for issues (e.g., incorrect scale, missing textures) +4. *Optimization* - Models are optimized for runtime performance +5. *Conversion* - Development assets are converted to production-ready formats +6. *Integration* - Assets are imported into the engine +7. *Runtime Loading* - The engine loads assets efficiently during execution + +When designing an asset pipeline, consider these important factors: + +==== File Format Selection + +Different file formats offer different trade-offs: + +1. *Interchange Formats* (e.g., glTF, FBX, Collada) + - Pros: Widely supported by modeling tools, preserve most data + - Cons: May contain unnecessary data, not optimized for runtime + +2. *Runtime Formats* (e.g., glb, engine-specific binary formats) + - Pros: Optimized for loading speed and memory usage + - Cons: May not be editable outside the engine + +==== Texture Compression + +Texture compression is crucial for performance: + +1. *Development Formats* (e.g., PNG, JPEG) + - Pros: Lossless or high quality, widely supported by editing tools + - Cons: Large file sizes, not optimized for GPU + +2. *Runtime Formats* (e.g., ktx, compressed GPU formats) + - Pros: Smaller file sizes, directly usable by GPU + - Cons: May have quality loss, platform-specific considerations + +==== Asset Bundling + +Consider how assets are packaged: + +1. *Separate Files* + - Pros: Easier to update individual assets, simpler version control + - Cons: More file operations, potential for missing dependencies + +2. *Bundled Assets* + - Pros: Fewer file operations, guaranteed dependencies + - Cons: Larger atomic updates, more complex version control + +=== Artist-Engine Collaboration Concepts + +Successful integration of art assets into a rendering engine requires clear communication and established workflows between artists and programmers. Here are key concepts to consider: + +==== Technical Specifications + +Regardless of the specific engine, you'll need to define: + +1. *Coordinate System* - Different applications use different coordinate systems (e.g., Y-up vs. Z-up) +2. *Scale* - Establish a consistent scale (e.g., 1 unit = 1 meter or 1 unit = 1 foot) +3. *Origin Placement* - Define where the origin point should be for different asset types +4. *Level of Detail* - Specify polygon count ranges for different asset types and usage scenarios + +==== Workflow Documentation + +Create documentation that addresses: + +1. *Naming Conventions* - Consistent naming helps with organization and automation +2. *Material Standards* - Define how materials should be structured (e.g., PBR parameters) +3. *Export Settings* - Document the correct export settings for your chosen interchange formats +4. *Quality Checklists* - Provide criteria for validating assets before submission + +==== Technical Art Bridge + +Consider establishing a technical art role that: + +1. Creates tools to streamline the art-to-engine pipeline +2. Validates assets before they enter the engine +3. Provides feedback to artists on technical requirements +4. Helps troubleshoot issues when assets don't appear correctly in-engine + +=== Development to Production Concepts + +The transition from artist-friendly development assets to optimized production assets involves several important concepts: + +==== Development vs. Production Assets + +Understanding the different needs at each stage: + +1. *Development Assets* + - Prioritize editability and iteration speed + - Use formats that are widely supported by content creation tools + - May be larger and less optimized for runtime performance + - Focus on preserving maximum quality and information + +2. *Production Assets* + - Prioritize runtime performance and memory efficiency + - Use formats optimized for the target platform(s) + - Apply appropriate compression and optimization techniques + - Balance quality against performance requirements + +==== Asset Validation + +Implement validation at key points in the pipeline: + +1. *Pre-Submission Validation* + - Check for adherence to technical specifications + - Verify that all required textures and materials are present + - Ensure proper scale, orientation, and origin placement + +2. *Pre-Conversion Validation* + - Verify that assets can be successfully processed by conversion tools + - Check for issues that might cause problems during conversion + +3. *Post-Conversion Validation* + - Verify that converted assets maintain visual fidelity + - Check for performance issues or memory consumption problems + - Ensure compatibility with target platforms + +==== Automation Considerations + +As projects grow, automation becomes increasingly important: + +1. *Batch Processing* + - Develop scripts or tools to process multiple assets at once + - Implement automated validation checks + +2. *Continuous Integration* + - Consider integrating asset processing into your CI/CD pipeline + - Automatically validate and convert assets when they're committed + +3. *Versioning* + - Track changes to assets and their processed versions + - Implement dependency tracking to rebuild only what's necessary + +=== Implementation Considerations + +When implementing a model loading system in any rendering engine, several key considerations should guide your approach: + +==== Abstraction Layers + +Design your model loading system with appropriate abstraction layers: + +1. *File Format Layer* + - Handles parsing specific file formats (e.g., glTF, FBX) + - Isolates format-specific code to make supporting multiple formats easier + - Converts from file format structures to your engine's internal structures + +2. *Resource Management Layer* + - Manages memory and GPU resources + - Handles caching and reference counting + - Provides a consistent interface regardless of the underlying file format + +3. *Scene Graph Layer* + - Organizes models in a hierarchical structure + - Manages transformations and parent-child relationships + - Facilitates operations like culling and scene traversal + +==== Performance Considerations + +Balance flexibility with performance: + +1. *Asynchronous Loading* + - Consider loading models in background threads to avoid blocking the main thread + - Implement a system for handling partially loaded models + +2. *Memory Management* + - Develop strategies for handling large models + - Consider level-of-detail (LOD) systems for complex scenes + - Implement streaming for very large environments + +3. *Batching and Instancing* + - Group similar models for efficient rendering + - Use instancing for repeated elements + +==== Extensibility + +Design for future expansion: + +1. *Material System* + - Create a flexible material system that can represent various shading models + - Support both simple and complex materials + +2. *Animation System* + - Design for different animation types (skeletal, morph targets, etc.) + - Consider how animations will interact with physics and gameplay systems + +3. *Custom Data* + - Allow for engine-specific metadata to be associated with models + - Support custom properties for gameplay or rendering purposes + +Understanding these concepts provides a solid foundation for designing and implementing model loading systems in any rendering engine. By carefully considering abstraction, performance, and extensibility from the beginning, you can create a robust system that will scale with your project's needs and adapt to changing requirements. + +== Our Project Implementation + +Now that we've explored the general concepts of asset pipelines, let's discuss how our specific project will implement these concepts. + +=== File Formats and Directory Structure + +For our engine, we'll use the following file formats and directory structure: + +1. *Model Format*: We'll use glTF 2.0 binary format (.glb) with embedded KTX2 textures. This format offers several advantages: + - Compact binary representation for efficient storage and loading + - Ability to embed textures, reducing file operations + - Support for animations, skinning, and PBR materials + - Industry standard with wide tool support + +2. *Texture Format*: We'll use KTX2 with Basis Universal compression for textures, which provides: + - Significant size reduction compared to PNG/JPEG + - GPU-ready formats that can be directly uploaded + - Cross-platform compatibility through transcoding + - Support for mipmaps and various compression formats + +3. *Directory Structure*: +[source] +---- +assets/ + ├── models/ // 3D model files + │ ├── characters/ // Character models + │ │ └── viking.glb // Example character model + │ ├── environments/ // Environment models + │ │ └── room.glb // Example environment model + │ └── props/ // Prop models + │ └── furniture.glb // Example prop model + └── shaders/ // Shader files + └── pbr.slang // PBR shader +---- + +=== Tools and Libraries + +We'll use the following tools and libraries to implement our asset pipeline: + +1. *Model Loading*: We'll use the tinygltf library to parse glTF files. This library provides: + - Comprehensive support for the glTF 2.0 specification + - Efficient parsing of binary glTF files + - Access to all glTF components (meshes, materials, animations, etc.) + +2. *Texture Loading*: We'll use the KTX-Software library to load KTX2 textures, which offers: + - Support for loading and transcoding Basis Universal compressed textures + - Efficient mipmap handling + - Integration with Vulkan texture formats + +3. *Asset Conversion*: For converting development assets to production assets, we'll use: + - KTX-Tools for texture conversion (PNG/JPEG to KTX2) + - glTF-Transform for model processing and optimization + - Custom scripts for automating the conversion process + +=== Integration with Engine Architecture + +Our model loading system will integrate with the engine architecture from previous chapters: + +1. *Resource Management*: We'll leverage the resource management system from the Engine Architecture chapter to: + - Cache loaded models and textures + - Implement reference counting for efficient memory management + - Support asynchronous loading of models + +2. *Component System*: We'll create the following components: + - ModelComponent: Manages model rendering and animation + - MaterialComponent: Handles material properties and textures + - These components will work with the TransformComponent from the Camera Transformations chapter + +3. *Rendering Pipeline*: Our model loading system will integrate with the rendering pipeline by: + - Providing mesh data for the geometry pass + - Supporting PBR materials for the lighting pass + - Enabling instanced rendering for repeated models + +=== Artist Workflow + +Our workflow for artists will be: + +1. *Development Phase*: + - Artists create models in tools like Blender or Maya + - Export to standard glTF (.gltf) with separate PNG/JPEG textures + - Test with glTF viewers to ensure correct appearance + +2. *Technical Requirements*: + - Right-handed coordinate system with Y-up + - 1 unit = 1 meter scale + - PBR materials using the metallic-roughness workflow + - Textures with power-of-two dimensions + +3. *Conversion Process*: + - Validate models against technical requirements + - Convert textures to KTX2 with Basis Universal compression + - Embed textures into glb files + - Optimize models (remove unused vertices, compress meshes, etc.) + +4. *Integration*: + - Place converted assets in the appropriate directories + - Register assets in the resource management system + - Create entities with appropriate components + +=== Runtime Loading + +At runtime, our engine will: + +1. *Load Models*: + - Parse glb files using tinygltf + - Extract mesh data, materials, and animations + - Create Vulkan buffers for vertices and indices + +2. *Process Materials*: + - Load embedded KTX2 textures + - Create Vulkan image views and samplers + - Set up descriptor sets for PBR rendering + +3. *Handle Animations*: + - Parse animation data from glTF + - Implement skeletal animation system + - Support animation blending and transitions + +4. *Render Models*: + - Use the scene graph to organize models hierarchically + - Apply transformations from the transform component + - Render with appropriate materials and shaders + +By implementing these specific approaches, our engine will have a robust and efficient asset pipeline that aligns with the general concepts discussed earlier in this chapter. + +link:03_model_system.adoc[Next: Implementing the Model Loading System] diff --git a/en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc b/en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc new file mode 100644 index 00000000..f24750cb --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc @@ -0,0 +1,396 @@ +::pp: {plus}{plus} + += Loading Models: Implementing the Model Loading System +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Implementing the Model Loading System + +=== Building on glTF Knowledge + +As we learned in the link:../../15_GLTF_KTX2_Migration.html[glTF and KTX2 Migration chapter], glTF is a modern 3D format that supports a wide range of features including PBR materials, animations, and scene hierarchies. In this chapter, we'll leverage these capabilities to build a more robust engine. + +While the previous chapter covered the basics of loading glTF models, here we'll focus on organizing the loaded data into a proper scene graph and implementing animation support. This approach will allow us to create more complex and dynamic scenes. + +In this chapter, we'll not only implement the technical aspects of model loading but also discuss the architectural decisions behind our design and how developers can effectively use this system in their applications. Understanding these concepts is crucial for building a maintainable and extensible engine. + +=== Setting Up Our Engine's Model System + +We'll start with the same tinygltf library setup as in the previous chapter: + +[source,cpp] +---- +// Include tinygltf for model loading +#include +---- + +However, instead of just loading the model data directly into vertex and index buffers, we'll create a more structured approach with proper data classes to represent our scene. + +=== Defining Data Structures + +To handle the rich data provided by glTF, we need to define several data structures: + +[source,cpp] +---- +// Vertex structure with position, normal, color, and texture coordinates +struct Vertex { + glm::vec3 pos; + glm::vec3 normal; + glm::vec3 color; + glm::vec2 texCoord; + + // Binding and attribute descriptions for Vulkan + static vk::VertexInputBindingDescription getBindingDescription() { + return { 0, sizeof(Vertex), vk::VertexInputRate::eVertex }; + } + + static std::array getAttributeDescriptions() { + return { + vk::VertexInputAttributeDescription( 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) ), + vk::VertexInputAttributeDescription( 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal) ), + vk::VertexInputAttributeDescription( 2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) ), + vk::VertexInputAttributeDescription( 3, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord) ) + }; + } + + // Equality operator and hash function for vertex deduplication + bool operator==(const Vertex& other) const { + return pos == other.pos && normal == other.normal && color == other.color && texCoord == other.texCoord; + } +}; + +// Structure for PBR material properties +struct Material { + glm::vec4 baseColorFactor = glm::vec4(1.0f); + float metallicFactor = 1.0f; + float roughnessFactor = 1.0f; + glm::vec3 emissiveFactor = glm::vec3(0.0f); + + int baseColorTextureIndex = -1; + int metallicRoughnessTextureIndex = -1; + int normalTextureIndex = -1; + int occlusionTextureIndex = -1; + int emissiveTextureIndex = -1; +}; + +// Structure for a mesh with vertices, indices, and material +struct Mesh { + std::vector vertices; + std::vector indices; + int materialIndex = -1; +}; +---- + +=== Why We Need a Scene Graph + +A scene graph is a tree-like data structure that organizes the spatial representation of a graphical scene. While it might seem tempting to use a simple collection or map to store 3D objects, scene graphs offer several critical advantages: + +==== Benefits of Using a Scene Graph + +* *Hierarchical Transformations*: Scene graphs allow child objects to inherit transformations from their parents. When you move, rotate, or scale a parent node, all its children are automatically transformed relative to the parent. This is essential for complex models like characters where moving the torso should also move the attached limbs. + +* *Spatial Organization*: Scene graphs organize objects based on their spatial relationships, making it easier to perform operations like culling, collision detection, and level-of-detail management. + +* *Animation Support*: Hierarchical structures are crucial for skeletal animations, where movements propagate through a chain of bones. + +* *Scene Management*: Scene graphs facilitate operations like saving/loading scenes, instancing (reusing the same model in different locations), and dynamic scene modifications. + +==== Scene Graphs vs. Simple Collections + +Unlike a simple map or array of objects, a scene graph: + +* Maintains parent-child relationships between objects +* Automatically propagates transformations down the hierarchy +* Provides a natural structure for traversal algorithms (rendering, picking, collision) +* Supports local-to-global coordinate transformations + +For example, with a flat collection of objects, if you wanted to move a character and all its equipment, you'd need to update each piece individually. With a scene graph, you simply move the character node, and all attached equipment moves automatically. + +==== Scene Graphs vs. Spatial Partitioning Systems (Game Maps) + +It's important to distinguish between scene graphs and spatial partitioning systems (often referred to as "game maps" in engine development): + +* *Scene Graphs* focus on hierarchical relationships and transformations between objects. +* *Spatial Partitioning Systems* focus on efficiently organizing objects in space for collision detection, visibility determination, and physics calculations. + +While scene graphs organize objects based on logical relationships (like a character and its equipment), spatial partitioning systems organize objects based on their physical location in the game world. + +===== Common Spatial Partitioning Systems + +Several spatial partitioning techniques are used in game development: + +* *Octrees*: Divide 3D space into eight equal octants recursively. Used for large open worlds where objects are distributed unevenly. Octrees adapt to object density, with more subdivisions in crowded areas. + +* *Binary Space Partitioning (BSP)*: Recursively divides space using planes. Particularly efficient for indoor environments and was popularized by early first-person shooters like Doom and Quake. + +* *Quadtrees*: The 2D equivalent of octrees, dividing space into four quadrants recursively. Commonly used for 2D games or for terrain in 3D games. + +* *Axis-Aligned Bounding Boxes (AABB) Trees*: Organize objects based on their bounding boxes, creating a hierarchy that allows for efficient collision checks. + +* *Portal Systems*: Divide the world into "rooms" connected by "portals." This approach is particularly effective for indoor environments with distinct areas. + +* *Spatial Hashing*: Maps 3D positions to a hash table, allowing for constant-time lookups of nearby objects. Useful for particle systems and other scenarios with many similar-sized objects. + +* *Bounding Volume Hierarchies (BVH)*: Create a tree of nested bounding volumes, allowing for efficient ray casting and collision detection. + +===== Spatial Partitioning in Popular Engines + +Different game engines use different spatial partitioning systems, often combining multiple approaches: + +* *Unreal Engine*: Uses a combination of octrees for the overall world and BSP for detailed indoor environments. Also uses a custom system called "Unreal Visibility Determination" that combines portals and potentially visible sets. + +* *Unity*: Implements a quadtree/octree hybrid system for its physics and rendering. For navigation, it uses a navigation mesh system. + +* *CryEngine/CRYENGINE*: Uses octrees for outdoor environments and portal systems for indoor areas. + +* *Godot*: Employs BVH trees for its physics engine and octrees for rendering. + +* *Source Engine (Valve)*: Famous for its Binary Space Partitioning (BSP) combined with a portal system called "Potentially Visible Set" (PVS). + +* *id Tech (id Software)*: Early versions (Doom, Quake) pioneered BSP usage. Later versions use combinations of BSP, octrees, and portal systems. + +* *Frostbite (EA)*: Uses a hierarchical grid system combined with octrees for its large-scale destructible environments. + +In practice, many modern engines use hybrid approaches, selecting the appropriate partitioning system based on the specific needs of different parts of the game world. + +=== Architectural Decisions + +When designing our model system, we made several key architectural decisions: + +* *Node-Based Structure*: We use a node-based approach where each node can have a mesh, transformation, and children. This provides flexibility for complex scene hierarchies. + +* *Separation of Concerns*: We separate geometric data (vertices, indices) from material properties and transformations, allowing for more efficient memory use and easier updates. + +* *Animation-Ready*: Our design includes dedicated structures for animations, supporting keyframe interpolation and different animation channels (translation, rotation, scale). + +* *Memory Management*: We use a centralized ownership model where the Model class owns all nodes, simplifying cleanup and preventing memory leaks. + +* *Efficient Traversal*: We maintain both a hierarchical structure (`nodes`) and a flat list (`linearNodes`) to support different traversal patterns efficiently. + +=== How Developers Would Use the Model System + +Here's how a developer would typically use this model system in their application: + +==== Loading and Initializing Models + +[source,cpp] +---- +// Create and load a model +Model* characterModel = new Model(); +loadFromFile(characterModel, "character.gltf"); + +// Find specific nodes in the model +Node* headNode = characterModel->findNode("Head"); +Node* weaponAttachPoint = characterModel->findNode("RightHand"); + +// Attach additional objects to the model +Model* weaponModel = new Model(); +loadFromFile(weaponModel, "weapon.gltf"); +weaponAttachPoint->children.push_back(weaponModel->nodes[0]); +---- + +==== Updating and Animating Models + +[source,cpp] +---- +// Play an animation +float deltaTime = 0.016f; // 16ms or ~60 FPS NB: Keep this relative to frame +instead of a constant in actual code as some systems are faster resulting in +faster animation on a constant that isn't tied to the frame time. +characterModel->updateAnimation(0, deltaTime); // Play the first animation + +// Manually transform nodes +headNode->rotation = glm::rotate(headNode->rotation, glm::radians(15.0f), glm::vec3(0, 1, 0)); // Look to the side +---- + +==== Rendering Models + +[source,cpp] +---- +void renderModel(Model* model, VkCommandBuffer commandBuffer) { + // Traverse all nodes in the model + for (auto& node : model->linearNodes) { + if (node->mesh.indices.size() > 0) { + // Get the global transformation matrix + glm::mat4 nodeMatrix = node->getGlobalMatrix(); + + // Update uniform buffer with the node's transformation + updateUniformBuffer(nodeMatrix); + + // Bind the appropriate material + if (node->mesh.materialIndex >= 0) { + bindMaterial(model->materials[node->mesh.materialIndex]); + } + + // Draw the mesh + vkCmdDrawIndexed(commandBuffer, + static_cast(node->mesh.indices.size()), + 1, 0, 0, 0); + } + } +} +---- + +=== Back to our tutorial + +Now that you've seen how the model system API is used from a hypothetical +developer's perspective, it's time to implement this functionality. +In the following sections, we'll guide you through implementing the scene +graph, animation system, and model class that will power the engine. + +=== Implementing a Scene Graph + +Now let's look at the implementation of our scene graph structure: + +[source,cpp] +---- +// Structure for a node in the scene graph +struct Node { + Node* parent = nullptr; + std::vector children; + Mesh mesh; + glm::mat4 matrix = glm::mat4(1.0f); + + // For animation + glm::vec3 translation = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 scale = glm::vec3(1.0f); + + glm::mat4 getLocalMatrix() { + return glm::translate(glm::mat4(1.0f), translation) * + glm::toMat4(rotation) * + glm::scale(glm::mat4(1.0f), scale) * + matrix; + } + + glm::mat4 getGlobalMatrix() { + glm::mat4 m = getLocalMatrix(); + Node* p = parent; + while (p) { + m = p->getLocalMatrix() * m; + p = p->parent; + } + return m; + } +}; +---- + +=== Animation Structures + +To support animations, we need additional structures: + +[source,cpp] +---- +// Structure for animation keyframes +struct AnimationChannel { + enum PathType { TRANSLATION, ROTATION, SCALE }; + PathType path; + Node* node = nullptr; + uint32_t samplerIndex; +}; + +// Structure for animation interpolation +struct AnimationSampler { + enum InterpolationType { LINEAR, STEP, CUBICSPLINE }; + InterpolationType interpolation; + std::vector inputs; // Key frame timestamps + std::vector outputsVec4; // Key frame values (for rotations) + std::vector outputsVec3; // Key frame values (for translations and scales) +}; + +// Structure for animation +struct Animation { + std::string name; + std::vector samplers; + std::vector channels; + float start = std::numeric_limits::max(); + float end = std::numeric_limits::min(); + float currentTime = 0.0f; +}; +---- + +=== The Model Class + +Now we can define a Model class that brings everything together: + +[source,cpp] +---- +// Structure for a model with nodes, meshes, materials, textures, and animations +struct Model { + std::vector nodes; + std::vector linearNodes; + std::vector materials; + std::vector animations; + + ~Model() { + for (auto node : linearNodes) { + delete node; + } + } + + Node* findNode(const std::string& name) { + for (auto node : linearNodes) { + if (node->name == name) { + return node; + } + } + return nullptr; + } + + void updateAnimation(uint32_t index, float deltaTime) { + if (animations.empty() || index >= animations.size()) { + return; + } + + Animation& animation = animations[index]; + animation.currentTime += deltaTime; + if (animation.currentTime > animation.end) { + animation.currentTime = animation.start; + } + + for (auto& channel : animation.channels) { + AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; + + // Find the current key frame + for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { + if (animation.currentTime >= sampler.inputs[i] && animation.currentTime <= sampler.inputs[i + 1]) { + float t = (animation.currentTime - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]); + + switch (channel.path) { + case AnimationChannel::TRANSLATION: { + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->translation = glm::mix(start, end, t); + break; + } + case AnimationChannel::ROTATION: { + glm::quat start = glm::quat(sampler.outputsVec4[i].w, sampler.outputsVec4[i].x, sampler.outputsVec4[i].y, sampler.outputsVec4[i].z); + glm::quat end = glm::quat(sampler.outputsVec4[i + 1].w, sampler.outputsVec4[i + 1].x, sampler.outputsVec4[i + 1].y, sampler.outputsVec4[i + 1].z); + channel.node->rotation = glm::slerp(start, end, t); + break; + } + case AnimationChannel::SCALE: { + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->scale = glm::mix(start, end, t); + break; + } + } + break; + } + } + } + } +}; +---- + +=== Next Steps: Loading glTF Files + +Now that we've designed our model system's architecture and implemented the core data structures, the next step is to actually load 3D models from glTF files. In the next chapter, we'll explore how to parse glTF files using the tinygltf library and populate our scene graph with the loaded data. We'll learn how to extract meshes, materials, textures, and animations from glTF files and convert them into our engine's internal representation. + +link:02_project_setup.adoc[Previous: Setting Up the Project] | link:04_loading_gltf.adoc[Next: Loading a glTF Model] diff --git a/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc new file mode 100644 index 00000000..ba63435a --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc @@ -0,0 +1,760 @@ +::pp: {plus}{plus} + += Loading Models: Understanding glTF +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Understanding glTF + +=== What is glTF? + +glTF (GL Transmission Format) is a standard 3D file format developed by the Khronos Group (the same organization behind OpenGL and Vulkan). It's often called the "JPEG of 3D" because it aims to be a universal, efficient format for 3D content. + +The main purpose of glTF is to bridge the gap between 3D content creation tools (like Blender, Maya, 3ds Max) and real-time rendering applications like games and visualization tools. Before glTF, developers often had to create custom exporters or use intermediate formats that weren't optimized for real-time rendering. + +Key advantages of glTF include: + +* *Efficiency*: Optimized for loading speed and rendering performance with minimal processing +* *Completeness*: Contains geometry, materials, textures, animations, and scene hierarchy in a single format +* *PBR Support*: Built-in support for modern physically-based rendering materials +* *Standardization*: Widely adopted across the industry, reducing the need for custom exporters +* *Extensibility*: Supports extensions for vendor-specific features while maintaining compatibility + +=== glTF File Structure and Data Organization + +A glTF file contains several key components organized in a structured way: + +* *Scenes and Nodes*: The hierarchical structure that organizes objects in a scene graph +* *Meshes*: The 3D geometry data (vertices, indices, attributes like normals and UVs) +* *Materials*: Surface properties using a physically-based rendering (PBR) model +* *Textures and Images*: Visual data for materials, with support for various texture types +* *Animations*: Keyframe data for animating nodes (position, rotation, scale) +* *Skins*: Data for skeletal animations (joint hierarchies and vertex weights) +* *Cameras*: Perspective or orthographic camera definitions + +==== The Buffer System: Efficient Binary Data Storage + +One of glTF's most powerful features is its three-level buffer system: + +1. *Buffers*: Raw binary data blocks (like files on disk) +2. *BufferViews*: Views into buffers with specific offset and length +3. *Accessors*: Descriptions of how to interpret data in a bufferView (type, component type, count, etc.) + +This system allows different attributes (positions, normals, UVs) to share the same underlying buffer, reducing memory usage and file size. For example: + +* A single buffer might contain all vertex data +* One bufferView points to the position data within that buffer +* Another bufferView points to the normal data +* Accessors describe how to interpret each bufferView (e.g., as vec3 floats) + +=== Using the tinygltf Library for Efficient Parsing + +Rather than writing a glTF parser from scratch (which would be a significant undertaking), we'll use the tinygltf library: + +* It's a lightweight, header-only C++ library that's easy to integrate +* It handles both .gltf and .glb formats transparently +* It manages the complex task of parsing JSON and binary data +* It provides a clean API for accessing all glTF components +* It handles the details of the buffer system, including base64-encoded data + +Using tinygltf allows us to focus on the higher-level task of converting the parsed data into our engine's structures rather than dealing with the low-level details of parsing JSON and binary data. + +=== Implementing a Robust glTF Loader + +When implementing a production-ready glTF loader, several considerations come into play: + +* *Error Handling*: Robust handling of malformed files and graceful failure +* *Format Detection*: Supporting both .gltf and .glb formats +* *Memory Management*: Efficient allocation and handling of large data +* *Extension Support*: Handling optional glTF extensions + +Let's look at how we implement the initial file loading: + +[source,cpp] +---- +void loadModel(const std::string& modelPath) { + // Create a tinygltf loader + tinygltf::Model gltfModel; + tinygltf::TinyGLTF loader; + std::string err, warn; + + // Detect file extension to determine which loader to use + bool ret = false; + std::string extension = modelPath.substr(modelPath.find_last_of(".") + 1); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + + if (extension == "glb") { + ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, modelPath); + } else if (extension == "gltf") { + ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, modelPath); + } else { + err = "Unsupported file extension: " + extension + ". Expected .gltf or .glb"; + } + + // Handle errors and warnings + if (!warn.empty()) { + std::cout << "glTF warning: " << warn << std::endl; + } + if (!err.empty()) { + std::cout << "glTF error: " << err << std::endl; + } + if (!ret) { + throw std::runtime_error("Failed to load glTF model"); + } + + // Clear existing model data + model = Model(); + + // Process the loaded data (covered in the following sections) +} +---- + +Supporting both .gltf and .glb formats gives artists flexibility in their workflow. + +glTF comes in two formats, each with its own advantages: + +* *.gltf*: A JSON-based format with external binary and image files +- Human-readable and easier to debug +- Allows for easier asset management (textures as separate files) +- Better for development workflows +* *.glb*: A binary format that combines everything in a single file +- More compact and efficient for distribution +- Reduces the number of file operations during loading +- Better for deployment and distribution + +=== Understanding Physically Based Rendering (PBR) Materials + +Materials define how surfaces look when rendered. Modern games and engines use Physically Based Rendering (PBR), which simulates how light interacts with real-world materials based on physical principles. + +==== The Evolution of Material Systems + +Material systems in 3D graphics have evolved significantly: + +1. *Basic Materials (1990s)*: Simple diffuse colors with optional specular highlights +2. *Multi-Texture Materials (2000s)*: Multiple texture maps combined for different effects +3. *Shader-Based Materials (Late 2000s)*: Custom shader programs for advanced effects +4. *Physically Based Rendering (2010s)*: Materials based on physical properties of real-world surfaces + +PBR represents the current state of the art in real-time graphics. It provides more realistic results across different lighting conditions and ensures consistent appearance regardless of the environment. + +==== Key PBR Material Properties + +The PBR model in glTF is based on the "metallic-roughness" workflow, which uses these key properties: + +* *Base Color*: The albedo or diffuse color of the surface (RGB or texture) +* *Metalness*: How metal-like the surface is (0.0 = non-metal, 1.0 = metal) + - Metals have no diffuse reflection but high specular reflection + - Non-metals (dielectrics) have diffuse reflection and minimal specular reflection +* *Roughness*: How smooth or rough the surface is (0.0 = mirror-like, 1.0 = rough) + - Controls the microsurface detail that causes light scattering + - Affects the sharpness of reflections and specular highlights +* *Normal Map*: Adds surface detail without extra geometry + - Perturbs surface normals to create the illusion of additional detail + - More efficient than adding actual geometry +* *Occlusion Map*: Approximates self-shadowing within surface crevices + - Darkens areas that would receive less ambient light + - Enhances the perception of depth and detail +* *Emissive*: Makes the surface emit light (RGB or texture) + - Used for glowing objects like screens, lights, or neon signs + - Not affected by scene lighting + +These properties can be specified as constant values or as texture maps for +spatial variation across the surface. We'll go into details about PBR in the +next few chapters. + +==== Texture Formats and Compression + +In our engine, we use KTX2 with Basis Universal compression for textures. This approach offers several advantages: + +* *Reduced File Size*: Basis Universal compression significantly reduces texture sizes while maintaining visual quality +* *GPU-Ready Formats*: KTX2 textures can be directly transcoded to platform-specific GPU formats +* *Cross-Platform Compatibility*: Basis Universal textures work across different platforms and graphics APIs +* *Mipmap Support*: KTX2 includes support for mipmaps, improving rendering quality and performance + +===== Embedded Textures in glTF/glb + +The glTF format supports two ways to include textures: + +1. *External References*: The .gltf file references external image files +2. *Embedded Data*: Images are embedded directly in the .glb file as binary data + +For our engine, we use the .glb format with embedded KTX2 textures. This approach: + +* Reduces the number of file operations during loading +* Ensures all textures are always available with the model +* Simplifies asset management and distribution + +The glTF specification supports embedded textures through the `bufferView` property of image objects. When using KTX2 textures, the `mimeType` is set to `"image/ktx2"` to indicate the format. + +Here's how we extract material data and load embedded KTX2 textures from a glTF file: + +[source,cpp] +---- +// First, load all textures from the model +std::vector textures; +for (size_t i = 0; i < gltfModel.textures.size(); i++) { + const auto& texture = gltfModel.textures[i]; + const auto& image = gltfModel.images[texture.source]; + + Texture tex; + tex.name = image.name.empty() ? "texture_" + std::to_string(i) : image.name; + + // Check if the image is embedded as KTX2 + if (image.mimeType == "image/ktx2" && image.bufferView >= 0) { + // Get the buffer view that contains the KTX2 data + const auto& bufferView = gltfModel.bufferViews[image.bufferView]; + const auto& buffer = gltfModel.buffers[bufferView.buffer]; + + // Extract the KTX2 data from the buffer + const uint8_t* ktx2Data = buffer.data.data() + bufferView.byteOffset; + size_t ktx2Size = bufferView.byteLength; + + // Load the KTX2 texture using KTX-Software library + ktxTexture2* ktxTexture = nullptr; + KTX_error_code result = ktxTexture2_CreateFromMemory( + ktx2Data, ktx2Size, + KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, + &ktxTexture + ); + + if (result != KTX_SUCCESS) { + std::cerr << "Failed to load KTX2 texture: " << ktxErrorString(result) << std::endl; + continue; + } + + // If the texture uses Basis Universal compression, transcode it to a GPU-friendly format + if (ktxTexture->isCompressed && ktxTexture2_NeedsTranscoding(ktxTexture)) { + // Choose the appropriate format based on GPU capabilities + ktx_transcode_fmt_e transcodeFmt = KTX_TTF_BC7_RGBA; + + // For devices that don't support BC7, use alternatives + // if (!deviceSupportsBC7) { + // transcodeFmt = KTX_TTF_ASTC_4x4_RGBA; + // } + // if (!deviceSupportsASTC) { + // transcodeFmt = KTX_TTF_ETC2_RGBA; + // } + + // Transcode the texture + result = ktxTexture2_TranscodeBasis(ktxTexture, transcodeFmt, 0); + if (result != KTX_SUCCESS) { + std::cerr << "Failed to transcode KTX2 texture: " << ktxErrorString(result) << std::endl; + ktxTexture2_Destroy(ktxTexture); + continue; + } + } + + // Create Vulkan image, memory, and view + vk::Format format = static_cast(ktxTexture2_GetVkFormat(ktxTexture)); + vk::Extent3D extent{ + static_cast(ktxTexture->baseWidth), + static_cast(ktxTexture->baseHeight), + static_cast(ktxTexture->baseDepth) + }; + uint32_t mipLevels = ktxTexture->numLevels; + + // Create the Vulkan image + vk::ImageCreateInfo imageCreateInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = extent, + .mipLevels = mipLevels, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + // Create the image, allocate memory, and bind them + // ... (code omitted for brevity) + + // Upload the texture data to the image + ktxTexture2_VkUploadEx(ktxTexture, &ktxVulkanTexture, &vkDevice, &vkQueue, + &ktxVulkanDeviceMemory, &ktxVulkanImage, + &ktxVulkanImageView, &ktxVulkanImageLayout, + &ktxVulkanImageMemory); + + // Store the Vulkan resources in our texture object + tex.image = ktxVulkanImage; + tex.imageView = ktxVulkanImageView; + tex.memory = ktxVulkanImageMemory; + + // Clean up KTX resources + ktxTexture2_Destroy(ktxTexture); + } else { + // Handle other image formats or external references + // ... (code omitted for brevity) + } + + // Create a sampler for the texture + VkSamplerCreateInfo samplerInfo = {}; + // ... (code omitted for brevity) + + textures.push_back(tex); +} + +// Now load materials and associate them with textures +for (const auto& material : gltfModel.materials) { + Material mat; + + // Base color + if (material.pbrMetallicRoughness.baseColorFactor.size() == 4) { + mat.baseColorFactor.r = material.pbrMetallicRoughness.baseColorFactor[0]; + mat.baseColorFactor.g = material.pbrMetallicRoughness.baseColorFactor[1]; + mat.baseColorFactor.b = material.pbrMetallicRoughness.baseColorFactor[2]; + mat.baseColorFactor.a = material.pbrMetallicRoughness.baseColorFactor[3]; + } + + // Metallic and roughness factors + mat.metallicFactor = material.pbrMetallicRoughness.metallicFactor; + mat.roughnessFactor = material.pbrMetallicRoughness.roughnessFactor; + + // Associate textures with the material + if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.pbrMetallicRoughness.baseColorTexture.index]; + mat.baseColorTexture = &textures[texture.source]; + } + + if (material.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.pbrMetallicRoughness.metallicRoughnessTexture.index]; + mat.metallicRoughnessTexture = &textures[texture.source]; + } + + if (material.normalTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.normalTexture.index]; + mat.normalTexture = &textures[texture.source]; + } + + if (material.occlusionTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.occlusionTexture.index]; + mat.occlusionTexture = &textures[texture.source]; + } + + if (material.emissiveTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.emissiveTexture.index]; + mat.emissiveTexture = &textures[texture.source]; + } + + model.materials.push_back(mat); +} +---- + +Now, let's talk about how this all fits together. + +=== Understanding Scene Graphs and Hierarchical Transformations + +A scene graph is a hierarchical tree-like data structure that organizes the spatial representation of a 3D scene. It's a fundamental concept in computer graphics and game engines, serving as the backbone for organizing complex scenes. + +==== Why Scene Graphs Matter + +Scene graphs offer several critical advantages over flat collections of objects: + +* *Hierarchical Transformations*: Children inherit transformations from their parents, making it natural to model complex relationships +* *Spatial Organization*: Objects are organized based on their logical relationships, making scene management easier +* *Animation Support*: Hierarchical structures are crucial for skeletal animations and complex movement patterns +* *Efficient Traversal*: Enables optimized rendering, culling, and picking operations +* *Instancing Support*: The same object can appear multiple times with different transformations + +Consider these practical examples: + +1. *Character with Equipment*: When a character moves, all attached equipment (weapons, armor) should move with it. With a scene graph, you move the character node, and all child nodes automatically inherit the transformation. + +2. *Vehicle with Moving Parts*: A vehicle might have wheels that rotate independently while the whole vehicle moves. A scene graph makes this hierarchy of movements natural to express. + +3. *Articulated Animations*: Characters with skeletons need joints that move relative to their parent joints. A scene graph directly models this parent-child relationship. + +==== Transformations in Scene Graphs + +One of the most powerful aspects of scene graphs is how they handle transformations: + +* Each node has a *local transformation* relative to its parent +* The *global transformation* is calculated by combining the node's local transformation with its parent's global transformation +* This allows for intuitive modeling of complex hierarchical movements + +The transformation pipeline typically works like this: + +1. Each node stores its local transformation (translation, rotation, scale) +2. When rendering, we calculate the global transformation by multiplying with parent transformations +3. This global transformation is used to position the object in world space + +Here's how we build a scene graph from glTF data: + +[source,cpp] +---- +// First pass: create all nodes +for (size_t i = 0; i < gltfModel.nodes.size(); i++) { + const auto& node = gltfModel.nodes[i]; + model.linearNodes[i] = new Node(); + model.linearNodes[i]->index = static_cast(i); + model.linearNodes[i]->name = node.name; + + // Get transformation data + if (node.translation.size() == 3) { + model.linearNodes[i]->translation = glm::vec3( + node.translation[0], node.translation[1], node.translation[2] + ); + } + // ... handle rotation and scale +} + +// Second pass: establish parent-child relationships +for (size_t i = 0; i < gltfModel.nodes.size(); i++) { + const auto& node = gltfModel.nodes[i]; + for (int childIdx : node.children) { + model.linearNodes[childIdx]->parent = model.linearNodes[i]; + model.linearNodes[i]->children.push_back(model.linearNodes[childIdx]); + } +} +---- + +We use a two-pass approach to ensure all nodes exist before we try to link them together. + +=== Understanding 3D Geometry and Mesh Data + +3D models are represented as meshes - collections of vertices, edges, and faces that define the shape of an object. Understanding how this data is structured is crucial for efficient rendering. + +==== The Building Blocks of 3D Models + +The fundamental components of 3D geometry are: + +* *Vertices*: Points in 3D space that define the shape +* *Indices*: References to vertices that define how they connect to form triangles +* *Attributes*: Additional data associated with vertices: + - *Positions*: 3D coordinates (x, y, z) + - *Normals*: Direction vectors perpendicular to the surface (for lighting calculations) + - *Texture Coordinates (UVs)*: 2D coordinates for mapping textures onto the surface + - *Tangents and Bitangents*: Vectors used for normal mapping + - *Colors*: Per-vertex color data + - *Skinning Weights and Indices*: For skeletal animations + +Modern 3D graphics use triangle meshes because: + +* Triangles are always planar (three points define a plane) +* Triangles are the simplest polygon that can represent any surface +* Graphics hardware is optimized for triangle processing + +==== Mesh Organization in glTF + +glTF organizes mesh data in a way that's efficient for both storage and rendering: + +* *Meshes*: Collections of primitives that form a logical object +* *Primitives*: Individual parts of a mesh, each with its own material +* *Attributes*: Vertex data like positions, normals, and texture coordinates +* *Indices*: References to vertices that define triangles + +This organization allows for: + +* Efficient memory use through data sharing +* Material variation within a single mesh +* Optimized rendering through batching + +Here's how we extract mesh data: + +[source,cpp] +---- +// Load meshes +for (size_t i = 0; i < gltfModel.nodes.size(); i++) { + const auto& node = gltfModel.nodes[i]; + if (node.mesh >= 0) { + const auto& mesh = gltfModel.meshes[node.mesh]; + + // Process each primitive + for (const auto& primitive : mesh.primitives) { + Mesh newMesh; + + // Set material + if (primitive.material >= 0) { + newMesh.materialIndex = primitive.material; + } + + // Extract vertex positions, normals, and texture coordinates + // ... (code omitted for brevity) + + // Extract indices that define triangles + // ... (code omitted for brevity) + + // Assign the mesh to the node + model.linearNodes[i]->mesh = newMesh; + } + } +} +---- + +=== Understanding Animation Systems + +Animation is what transforms static 3D models into living, breathing entities in our virtual worlds. A robust animation system is essential for creating engaging and dynamic 3D applications. + +==== Animation Techniques in 3D Graphics + +Several animation techniques are commonly used in 3D graphics: + +* *Keyframe Animation*: Defining specific poses at specific times, with interpolation between them +* *Skeletal Animation*: Using a hierarchy of bones to deform a mesh +* *Morph Target Animation*: Interpolating between predefined mesh shapes +* *Procedural Animation*: Generating animation through algorithms and physics +* *Particle Systems*: Animating many small elements with simple rules + +Modern games typically use a combination of these techniques, with skeletal animation forming the backbone of character movement. + +==== Core Animation Concepts + +Several key concepts are fundamental to understanding animation systems: + +* *Keyframes*: Specific points in time where animation values are explicitly defined +* *Interpolation*: Calculating values between keyframes to create smooth motion +* *Channels*: Targeting specific properties (like position or rotation) for animation +* *Blending*: Combining multiple animations with different weights +* *Retargeting*: Applying animations created for one model to another + +==== The glTF Animation System + +glTF uses a flexible animation system that can represent various animation techniques: + +* *Animations*: Collections of channels and samplers +* *Channels*: Links between samplers and node properties (translation, rotation, scale) +* *Samplers*: Keyframe data with timestamps, values, and interpolation methods +* *Targets*: The properties being animated (translation, rotation, scale, or weights for morph targets) + +glTF supports three interpolation methods: +* *LINEAR*: Smooth transitions with constant velocity +* *STEP*: Sudden changes with no interpolation +* *CUBICSPLINE*: Smooth curves with control points for acceleration and deceleration + +This system allows for complex animations that can target specific parts of a model independently, enabling actions like walking, facial expressions, and complex interactions. + +Here's how we load animation data: + +[source,cpp] +---- +// Load animations +for (const auto& anim : gltfModel.animations) { + Animation animation; + animation.name = anim.name; + + // Load keyframe data + for (const auto& sampler : anim.samplers) { + AnimationSampler animSampler{}; + + // Set interpolation type (LINEAR, STEP, or CUBICSPLINE) + // ... (code omitted for brevity) + + // Extract keyframe times and values + // ... (code omitted for brevity) + + animation.samplers.push_back(animSampler); + } + + // Connect samplers to node properties + for (const auto& channel : anim.channels) { + AnimationChannel animChannel{}; + + // Set target node and property (translation, rotation, or scale) + // ... (code omitted for brevity) + + animation.channels.push_back(animChannel); + } + + model.animations.push_back(animation); +} +---- + +=== Integration with the Rendering Pipeline + +Now that we've loaded our model data, let's discuss how it integrates with the rest of our rendering pipeline. + +==== From Asset Loading to Rendering + +The journey from a glTF file to pixels on the screen involves several stages: + +1. *Asset Loading*: The glTF loader populates our Model, Node, Mesh, and Material structures +2. *Scene Management*: The engine maintains a collection of loaded models in the scene +3. *Update Loop*: Each frame, animations are updated based on elapsed time +4. *Culling*: The engine determines which objects are potentially visible +5. *Rendering*: The scene graph is traversed, and each visible mesh is rendered with its material + +This pipeline allows for efficient rendering of complex scenes with animated models. + +==== Rendering Optimizations + +Several optimizations can improve the performance of model rendering: + +* *Batching*: Group similar objects to reduce draw calls +* *Instancing*: Render multiple instances of the same mesh with different transforms +* *Level of Detail (LOD)*: Use simpler versions of models at greater distances +* *Frustum Culling*: Skip rendering objects outside the camera's view +* *Occlusion Culling*: Skip rendering objects hidden behind other objects + +==== Memory Management Considerations + +When loading models, especially large ones, memory management becomes crucial: + +* *Vertex Data*: Store in GPU buffers for efficient rendering +* *Indices*: Use 16-bit indices when possible to save memory +* *Textures*: Use KTX2 with Basis Universal compression to significantly reduce memory usage +* *Instancing*: Reuse the same model data for multiple instances with different transforms + +===== Efficient Texture Memory Management with KTX2 and Basis Universal + +Textures often consume the majority of GPU memory in 3D applications. KTX2 with Basis Universal compression provides several memory optimization benefits: + +* *Supercompression*: Basis Universal can reduce texture size by 4-10x compared to uncompressed formats +* *GPU-Native Formats*: Textures are transcoded to formats that GPUs can directly sample from, avoiding runtime decompression +* *Mipmaps*: KTX2 supports mipmaps, which not only improve visual quality but also reduce memory usage for distant objects +* *Format Selection*: The transcoder can choose the optimal format based on the target GPU's capabilities: + - BC7 for desktop GPUs (NVIDIA, AMD, Intel) + - ASTC for mobile GPUs (ARM, Qualcomm) + - ETC2 for older mobile GPUs + +===== Integration with Vulkan Rendering Pipeline + +To efficiently integrate KTX2 textures with Vulkan: + +1. *Descriptor Sets*: Create descriptor sets that bind texture image views and samplers to shader binding points +2. *Pipeline Layout*: Define a pipeline layout that includes these descriptor sets +3. *Shader Access*: In shaders, access textures using the appropriate binding points + +Here's a simplified example of setting up descriptor sets for PBR textures: + +[source,cpp] +---- +// Create descriptor set layout for PBR textures +std::array bindings{ + // Base color texture + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Metallic-roughness texture + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Normal map + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Occlusion map + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Emissive map + vk::DescriptorSetLayoutBinding{ + .binding = 4, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + } +}; + +vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() +}; + +vk::raii::DescriptorSetLayout descriptorSetLayout(device, layoutInfo); + +// For each material, create a descriptor set and update it with the material's textures +for (const auto& material : model.materials) { + // Allocate descriptor set from the descriptor pool + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = descriptorPool, + .descriptorSetCount = 1, + .pSetLayouts = &*descriptorSetLayout + }; + + vk::raii::DescriptorSet descriptorSet = std::move(vk::raii::DescriptorSets(device, allocInfo).front()); + + // Update descriptor set with texture image views and samplers + std::vector descriptorWrites; + + if (material.baseColorTexture) { + vk::DescriptorImageInfo imageInfo{ + .sampler = material.baseColorTexture->sampler, + .imageView = material.baseColorTexture->imageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + vk::WriteDescriptorSet write{ + .dstSet = *descriptorSet, + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + descriptorWrites.push_back(write); + } + + // Similar writes for other textures + // ... + + device.updateDescriptorSets(descriptorWrites, {}); + + // Store the descriptor set with the material for later use during rendering + material.descriptorSet = *descriptorSet; +} +---- + +===== Best Practices for Texture Memory Management + +To optimize texture memory usage: + +1. *Texture Atlasing*: Combine multiple small textures into a single larger texture to reduce state changes +2. *Mipmap Management*: Generate and use mipmaps for all textures to improve performance and quality +3. *Texture Streaming*: For very large scenes, implement texture streaming to load higher resolution textures only when needed +4. *Memory Budgeting*: Implement a texture budget system that can reduce texture quality when memory is constrained +5. *Format Selection*: Choose the appropriate format based on the texture content: + - BC7/ASTC for color textures with alpha + - BC1/ETC1 for color textures without alpha + - BC5/ETC2 for normal maps + - BC4/EAC for single-channel textures (roughness, metallic, etc.) + +=== Summary and Next Steps + +In this chapter, we've explored the process of loading 3D models from glTF files and organizing them into a scene graph. We've covered: + +* The structure and advantages of the glTF format +* How to use the tinygltf library for efficient parsing +* The physically-based material system used in modern rendering +* How scene graphs organize objects in a hierarchical structure +* The representation of 3D geometry in meshes +* Animation systems for bringing models to life +* Integration with the rendering pipeline + +Our glTF loader creates a complete scene graph with: + +* Nodes organized in a hierarchy +* Meshes attached to nodes +* Materials defining surface properties +* Animations that can change node properties over time + +This structure allows us to: + +* Render complex 3D scenes +* Animate characters and objects +* Apply transformations that propagate through the hierarchy +* Optimize rendering for performance + +In the next chapter, we'll explore how to render these models using +physically-based rendering techniques, bringing our loaded assets to life +with realistic lighting and materials. + +link:03_model_system.adoc[Previous: Implementing the Model Loading System] | link:05_pbr_rendering.adoc[Next: Implementing PBR Rendering] diff --git a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc new file mode 100644 index 00000000..93183191 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc @@ -0,0 +1,576 @@ +::pp: {plus}{plus} + += Loading Models: Implementing PBR for glTF Models +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Applying PBR to glTF Models + +=== Building on PBR Knowledge + +In the link:../Lighting_Materials/01_introduction.adoc[Lighting & Materials chapter], we explored the fundamentals of Physically Based Rendering (PBR), including its core principles, the BRDF, and material properties. Now, we'll apply that knowledge to implement a PBR pipeline for the glTF models we've loaded. + +As we learned in the link:../../15_GLTF_KTX2_Migration.html[glTF and KTX2 Migration chapter], glTF uses PBR with the metallic-roughness workflow for its material system. This aligns perfectly with the PBR concepts we've already covered, making it straightforward to render our glTF models with physically accurate lighting. + +=== Leveraging glTF's PBR Materials + +The glTF format already includes all the material properties we need for PBR: + +* *Base Color*: Defined by the baseColorFactor and baseColorTexture +* *Metallic and Roughness*: Defined by metallicFactor, roughnessFactor, and metallicRoughnessTexture +* *Normal Maps*: For surface detail without additional geometry +* *Occlusion Maps*: For approximating ambient occlusion +* *Emissive Maps*: For self-illuminating parts of the material + +By using these properties directly, we can ensure our rendering matches the artist's intent and produces physically accurate results. + +=== Implementing PBR in Our Engine + +Now that we understand the theory behind PBR, let's implement it in our engine. We'll build on the material data we loaded from glTF files in the previous chapter. + +==== Uniform Buffer for PBR + +We need to extend our uniform buffer to include PBR parameters: + +[source,cpp] +---- +// Structure for uniform buffer object +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; + + // PBR parameters + alignas(16) glm::vec4 lightPositions[4]; // Position and radius + alignas(16) glm::vec4 lightColors[4]; // RGB color and intensity + alignas(16) glm::vec4 camPos; // Camera position for view-dependent effects + alignas(4) float exposure = 4.5f; // Exposure for HDR rendering + alignas(4) float gamma = 2.2f; // Gamma correction value + alignas(4) float prefilteredCubeMipLevels = 1.0f; // For image-based lighting + alignas(4) float scaleIBLAmbient = 1.0f; // Scale factor for ambient lighting +}; +---- + +This uniform buffer includes: + +1. *Standard Transformation Matrices*: Model, view, and projection matrices for vertex transformation +2. *Light Information*: Positions and colors of up to four light sources +3. *Camera Position*: Needed for view-dependent effects like Fresnel +4. *Rendering Parameters*: Exposure, gamma, and other values for post-processing +5. *Image-Based Lighting Parameters*: For environment reflections (we'll cover this in a later chapter) + +==== Push Constants for Materials + +We'll use link:https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#descriptorsets-pushconstant[push constants] to pass material properties to the shader: + +[source,cpp] +---- +// Structure for push constants +struct PushConstantBlock { + glm::vec4 baseColorFactor; // RGB base color and alpha + float metallicFactor; // How metallic the surface is + float roughnessFactor; // How rough the surface is + int baseColorTextureSet; // Texture coordinate set for base color + int physicalDescriptorTextureSet; // Texture coordinate set for metallic-roughness + int normalTextureSet; // Texture coordinate set for normal map + int occlusionTextureSet; // Texture coordinate set for occlusion + int emissiveTextureSet; // Texture coordinate set for emission + float alphaMask; // Whether to use alpha masking + float alphaMaskCutoff; // Alpha threshold for masking +}; +---- + +Push constants are ideal for material properties because: + +* They can be updated quickly between draw calls +* They don't require descriptor sets +* They're perfect for per-object data like material properties + +==== Setting Up the Descriptor Sets + +To implement PBR, we need to set up descriptor sets for our textures and uniform buffer: + +[source,cpp] +---- +// Create descriptor set layout +void createDescriptorSetLayout() { + // Binding for uniform buffer + vk::DescriptorSetLayoutBinding uboBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment + }; + + // Bindings for textures + std::array textureBindings{}; + + // Base color texture + textureBindings[0].binding = 1; + textureBindings[0].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[0].descriptorCount = 1; + textureBindings[0].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Metallic-roughness texture + textureBindings[1].binding = 2; + textureBindings[1].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[1].descriptorCount = 1; + textureBindings[1].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Normal map + textureBindings[2].binding = 3; + textureBindings[2].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[2].descriptorCount = 1; + textureBindings[2].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Occlusion map + textureBindings[3].binding = 4; + textureBindings[3].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[3].descriptorCount = 1; + textureBindings[3].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Emissive map + textureBindings[4].binding = 5; + textureBindings[4].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[4].descriptorCount = 1; + textureBindings[4].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Combine all bindings + std::array bindings = { + uboBinding, + textureBindings[0], + textureBindings[1], + textureBindings[2], + textureBindings[3], + textureBindings[4] + }; + + // Create the descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); +} +---- + +==== Setting Up the Pipeline + +Our PBR pipeline needs to be configured for the specific requirements of physically-based rendering: + +[source,cpp] +---- +void createPipeline() { + // ... (standard pipeline setup code) + + // Enable alpha blending + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = vk::True, + .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, + .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eZero, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = + vk::ColorComponentFlagBits::eR | + vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | + vk::ColorComponentFlagBits::eA + }; + + // Set up push constants for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(PushConstantBlock) + }; + + // Create the pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &descriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // ... (rest of pipeline creation) +} +---- + +=== PBR Shader Implementation + +The heart of our PBR implementation is in the fragment shader. Here's a simplified version of a PBR fragment shader written in Slang: + +[source,slang] +---- + +// Input from vertex shader +struct VSOutput { + float3 WorldPos : POSITION; // Automatically assigned to location 0 + float3 Normal : NORMAL; // Automatically assigned to location 1 + float2 UV : TEXCOORD0; // Automatically assigned to location 2 + float4 Tangent : TANGENT; // Automatically assigned to location 3 +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPositions[4]; + float4 lightColors[4]; + float4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +}; + +// Push constants for material properties +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +}; + +// Constants +static const float PI = 3.14159265359; + +// Bindings +ConstantBuffer ubo; +Texture2D baseColorMap; +SamplerState baseColorSampler; +Texture2D metallicRoughnessMap; +SamplerState metallicRoughnessSampler; +Texture2D normalMap; +SamplerState normalSampler; +Texture2D occlusionMap; +SamplerState occlusionSampler; +Texture2D emissiveMap; +SamplerState emissiveSampler; + +[[vk::push_constant]] PushConstants material; + +// PBR functions +float DistributionGGX(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH2 = NdotH * NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + + return ggx1 * ggx2; +} + +float3 FresnelSchlick(float cosTheta, float3 F0) { + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +// Main fragment shader function +float4 main(VSOutput input) : SV_TARGET +{ + // Sample material textures + float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float metallic = metallicRoughness.x * material.metallicFactor; + float roughness = metallicRoughness.y * material.roughnessFactor; + float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; // link:https://learnopengl.com/Advanced-Lighting/SSAO[Ambient occlusion] + float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; // link:https://learnopengl.com/PBR/Lighting[Emissive lighting] (self-illumination) + + // Calculate normal in link:https://learnopengl.com/Advanced-Lighting/Normal-Mapping[tangent space] + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + // Apply link:https://learnopengl.com/Advanced-Lighting/Normal-Mapping[normal mapping] + float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + float3 T = normalize(input.Tangent.xyz); + float3 B = normalize(cross(N, T)) * input.Tangent.w; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(tangentNormal, TBN)); + } + + // Calculate view and reflection vectors + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + float3 R = reflect(-V, N); + + // Calculate F0 (base reflectivity) + float3 F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + + // Initialize lighting + float3 Lo = float3(0.0, 0.0, 0.0); + + // Calculate lighting for each light + for (int i = 0; i < 4; i++) { + float3 lightPos = ubo.lightPositions[i].xyz; + float3 lightColor = ubo.lightColors[i].rgb; + + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + // Calculate half vector (the normalized vector halfway between view and light direction) + // Used in link:https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model[Blinn-Phong] and PBR models + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // link:https://learnopengl.com/PBR/Theory[Energy conservation] + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Add to outgoing radiance + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + + // Add ambient and emissive + float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + float3 color = ambient + Lo + emissive; + + // link:https://en.wikipedia.org/wiki/High-dynamic-range_rendering[HDR] link:https://en.wikipedia.org/wiki/Tone_mapping[tonemapping] and link:https://en.wikipedia.org/wiki/Gamma_correction[gamma correction] + color = color / (color + float3(1.0, 1.0, 1.0)); + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + return float4(color, baseColor.a); +} +---- + +This shader implements the core PBR lighting model, including: + +* Sampling material textures +* Calculating normal mapping +* Computing the specular BRDF with D, F, and G terms +* Applying energy conservation +* Handling multiple light sources +* Tone mapping and gamma correction + +==== Lighting Setup for PBR + +PBR requires careful setup of light sources to achieve realistic results. Here's how we can set up lights in our application: + +[source,cpp] +---- +void setupLights() { + // Set up four lights with different positions and colors + std::array lightPositions = { + glm::vec4(-10.0f, 10.0f, 10.0f, 1.0f), + glm::vec4(10.0f, 10.0f, 10.0f, 1.0f), + glm::vec4(-10.0f, -10.0f, 10.0f, 1.0f), + glm::vec4(10.0f, -10.0f, 10.0f, 1.0f) + }; + + std::array lightColors = { + glm::vec4(300.0f, 300.0f, 300.0f, 1.0f), // White + glm::vec4(300.0f, 300.0f, 0.0f, 1.0f), // Yellow + glm::vec4(0.0f, 0.0f, 300.0f, 1.0f), // Blue + glm::vec4(300.0f, 0.0f, 0.0f, 1.0f) // Red + }; + + // Update uniform buffer with light data + for (size_t i = 0; i < swapChainImages.size(); i++) { + UniformBufferObject ubo{}; + // ... (set up transformation matrices) + + // Set light positions and colors + for (int j = 0; j < 4; j++) { + ubo.lightPositions[j] = lightPositions[j]; + ubo.lightColors[j] = lightColors[j]; + } + + // Set camera position for view-dependent effects + ubo.camPos = glm::vec4(camera.getPosition(), 1.0f); + + // Set other PBR parameters + ubo.exposure = 4.5f; + ubo.gamma = 2.2f; + + // Copy to uniform buffer + memcpy(uniformBuffersMapped[i], &ubo, sizeof(ubo)); + } +} +---- + +==== Camera Integration for PBR + +PBR relies on view-dependent effects like the Fresnel effect, so we need to integrate our camera system: + +[source,cpp] +---- +void updateUniformBuffer(uint32_t currentImage) { + UniformBufferObject ubo{}; + + // Update transformation matrices + ubo.model = glm::mat4(1.0f); // Or get from the model's node + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + + // Vulkan's Y coordinate is inverted compared to OpenGL + ubo.proj[1][1] *= -1; + + // Update camera position for PBR calculations + ubo.camPos = glm::vec4(camera.getPosition(), 1.0f); + + // ... (update other PBR parameters) + + // Copy to uniform buffer + memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +} +---- + +=== Rendering with PBR + +Finally, let's put it all together to render our models with PBR: + +[source,cpp] +---- +void drawModel(vk::raii::CommandBuffer& commandBuffer, Model* model) { + // Bind descriptor set with uniform buffer and textures + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Traverse the model's scene graph + for (auto& node : model->linearNodes) { + if (node->mesh.indices.size() > 0) { + // Get the global transformation matrix + glm::mat4 nodeMatrix = node->getGlobalMatrix(); + + // Update model matrix in uniform buffer + // (In a real implementation, we'd use a separate UBO for each model) + + // Set up push constants for material properties + if (node->mesh.materialIndex >= 0) { + Material& mat = model->materials[node->mesh.materialIndex]; + + PushConstantBlock pushConstants{}; + pushConstants.baseColorFactor = mat.baseColorFactor; + pushConstants.metallicFactor = mat.metallicFactor; + pushConstants.roughnessFactor = mat.roughnessFactor; + pushConstants.baseColorTextureSet = mat.baseColorTextureIndex; + pushConstants.physicalDescriptorTextureSet = mat.metallicRoughnessTextureIndex; + pushConstants.normalTextureSet = mat.normalTextureIndex; + pushConstants.occlusionTextureSet = mat.occlusionTextureIndex; + pushConstants.emissiveTextureSet = mat.emissiveTextureIndex; + + commandBuffer.pushConstants( + pipelineLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + sizeof(PushConstantBlock), + &pushConstants + ); + } + + // Bind vertex and index buffers + vk::Buffer vertexBuffers[] = {*node->mesh.vertexBuffer}; + vk::DeviceSize offsets[] = {0}; + commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(*node->mesh.indexBuffer, 0, vk::IndexType::eUint32); + + // Draw the mesh + commandBuffer.drawIndexed( + static_cast(node->mesh.indices.size()), + 1, + 0, + 0, + 0 + ); + } + } +} +---- + +=== Advanced PBR Techniques + +While we've covered the basics of PBR implementation, there are several advanced techniques that can enhance the realism of your rendering: + +==== Image-Based Lighting (IBL) + +link:https://learnopengl.com/PBR/IBL/Diffuse-irradiance[IBL] uses environment maps to simulate global illumination: +* *Diffuse IBL*: Uses link:https://learnopengl.com/PBR/IBL/Diffuse-irradiance[irradiance maps] for ambient lighting +* *Specular IBL*: Uses link:https://learnopengl.com/PBR/IBL/Specular-IBL[pre-filtered environment maps] and link:https://learnopengl.com/PBR/IBL/Specular-IBL[BRDF integration maps] for reflections + +==== Subsurface Scattering + +For materials like skin, wax, or marble where light penetrates the surface: +* link:https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-14-advanced-techniques-realistic-real-time-skin[Simulates how light scatters within translucent materials] +* Can be approximated with techniques like subsurface scattering profiles + +==== Clear Coat + +For materials with a thin, glossy layer on top: +* link:https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat[Automotive paint, varnished wood, etc.] +* Implemented as an additional specular lobe + +==== Anisotropy + +For materials with directional reflections: +* link:https://google.github.io/filament/Filament.html#materialsystem/anisotropicmodel[Brushed metal, hair, fabric, etc.] +* Requires additional material parameters and modified BRDFs + +=== Conclusion and Next Steps + +In this chapter, we've applied the PBR knowledge from the Lighting & Materials chapter to implement a PBR pipeline for our glTF models. We've learned: + +* How to leverage the material properties from glTF for PBR rendering +* How to set up uniform buffers and push constants for PBR parameters +* How to implement a PBR shader that works with glTF materials +* How to integrate our camera system with PBR for view-dependent effects +* How to render glTF models with physically accurate lighting + +This implementation allows us to render the glTF models we loaded in the previous chapter with physically accurate materials, resulting in more realistic and consistent rendering across different lighting conditions. + +In the next chapter, we'll explore how to render multiple objects with different transformations, which will allow us to create more complex scenes with our PBR-enabled engine. + +If you want to dive deeper into lighting and materials, refer back to the Lighting & Materials chapter, where we explored the theory behind PBR in detail. + +link:04_loading_gltf.adoc[Previous: Loading a glTF Model] | link:06_multiple_objects.adoc[Next: Rendering Multiple Objects] diff --git a/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc new file mode 100644 index 00000000..7300ce5d --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc @@ -0,0 +1,786 @@ +::pp: {plus}{plus} + += Loading Models: Managing Multiple Objects +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Managing Multiple Objects in a 3D Scene + +=== Introduction to Multi-Object Rendering + +In previous chapters, we've focused on loading and rendering a single 3D model. However, real-world applications rarely display just one object. Games, simulations, and visualizations typically contain many objects that interact within a shared environment. This chapter explores how to efficiently manage and render multiple objects in a 3D scene. + +The ability to render multiple objects is fundamental to creating rich, interactive environments. It involves not just duplicating models, but also managing their unique properties, spatial relationships, and rendering states. As we'll see, this introduces both challenges and opportunities for optimization. + +=== Approaches to Managing Multiple Objects + +There are several strategies for handling multiple objects in a 3D engine, each with different trade-offs: + +==== Object Instances vs. Multiple Models + +When creating a scene with multiple similar objects (like trees in a forest or buildings in a city), we have two main approaches: + +* *Multiple Model Instances*: Load the model once but render it multiple times with different transformations + - Advantages: Memory efficient, single asset to manage + - Use cases: Repeated elements like trees, rocks, furniture + +* *Unique Models*: Load separate models for each unique object + - Advantages: Greater variety, independent modifications + - Use cases: Main characters, unique structures, varied elements + +For our engine, we'll implement the instancing approach, which is more memory-efficient and suitable for many common scenarios. + +==== Scene Organization Strategies + +Beyond simply having multiple objects, we need to organize them effectively: + +* *Flat Collection*: Store all objects in a simple list or array + - Advantages: Simplicity, easy iteration + - Disadvantages: No spatial relationships, inefficient for large scenes + +* *Spatial Partitioning*: Organize objects by their location in 3D space + - Advantages: Efficient culling and queries, better performance for large scenes + - Examples: Octrees, BSP trees, grid systems + +* *Scene Graph*: Organize objects in a hierarchical tree structure + - Advantages: Parent-child relationships, hierarchical transformations + - Use cases: Articulated models, complex object relationships + +Our implementation will use a simple collection for this example, but in a more advanced engine, you would typically combine this with spatial partitioning and scene graph techniques. + +=== Performance Considerations + +Rendering multiple objects efficiently requires careful attention to performance: + +==== Draw Call Optimization + +Each object typically requires at least one draw call, which can become a bottleneck: + +* *Batching*: Combining similar objects into a single draw call +* *Instanced Rendering*: Using hardware instancing to draw multiple copies of the same mesh +* *Level of Detail (LOD)*: Using simpler models for distant objects + +==== Culling Techniques + +Not all objects need to be rendered every frame: + +* *Frustum Culling*: Skip rendering objects outside the camera's view +* *Occlusion Culling*: Skip rendering objects hidden behind other objects +* *Distance Culling*: Skip rendering objects too far from the camera + +==== Memory Management + +With multiple objects, memory usage becomes more critical: + +* *Shared Resources*: Reuse meshes, textures, and materials across objects +* *Asset Streaming*: Load and unload assets based on proximity to the camera +* *Instance Data*: Store only transformation and material variations per instance + +=== Implementing Object Instances + +Now let's implement a system for managing multiple object instances. We'll start with a simple structure to store instance data: + +[source,cpp] +---- +// Object instances - using the same structure as in our model system +struct ObjectInstance { + glm::vec3 position; // Position in world space + glm::vec3 rotation; // Rotation in Euler angles (degrees) + glm::vec3 scale; // Scale factors for each axis +}; + +// Collection of object instances +std::vector objectInstances; +---- + +This structure stores the position, rotation, and scale for each instance, along with a method to compute the model matrix. The model matrix transforms the object from its local space to world space, combining all three transformations. + +Next, we'll set up several instances with different transformations: + +[source,cpp] +---- +void setupObjectInstances() { + // Create multiple instances of the model with different positions + const int MAX_OBJECTS = 10; // Define how many objects we want + objectInstances.resize(MAX_OBJECTS); + + // Instance 1 - Center + objectInstances[0].position = glm::vec3(0.0f, 0.0f, 0.0f); + objectInstances[0].rotation = glm::vec3(0.0f, 0.0f, 0.0f); + objectInstances[0].scale = glm::vec3(1.0f); + + // Instance 2 - Left + objectInstances[1].position = glm::vec3(-2.0f, 0.0f, -1.0f); + objectInstances[1].rotation = glm::vec3(0.0f, 45.0f, 0.0f); + objectInstances[1].scale = glm::vec3(0.8f); + + // Instance 3 - Right + objectInstances[2].position = glm::vec3(2.0f, 0.0f, -1.0f); + objectInstances[2].rotation = glm::vec3(0.0f, -45.0f, 0.0f); + objectInstances[2].scale = glm::vec3(0.8f); + + // Instance 4 - Back Left + objectInstances[3].position = glm::vec3(-1.5f, 0.0f, -3.0f); + objectInstances[3].rotation = glm::vec3(0.0f, 30.0f, 0.0f); + objectInstances[3].scale = glm::vec3(0.7f); + + // Instance 5 - Back Right + objectInstances[4].position = glm::vec3(1.5f, 0.0f, -3.0f); + objectInstances[4].rotation = glm::vec3(0.0f, -30.0f, 0.0f); + objectInstances[4].scale = glm::vec3(0.7f); + + // Instance 6 - Front Left + objectInstances[5].position = glm::vec3(-1.5f, 0.0f, 1.5f); + objectInstances[5].rotation = glm::vec3(0.0f, -30.0f, 0.0f); + objectInstances[5].scale = glm::vec3(0.6f); + + // Instance 7 - Front Right + objectInstances[6].position = glm::vec3(1.5f, 0.0f, 1.5f); + objectInstances[6].rotation = glm::vec3(0.0f, 30.0f, 0.0f); + objectInstances[6].scale = glm::vec3(0.6f); + + // Instance 8 - Above + objectInstances[7].position = glm::vec3(0.0f, 2.0f, -2.0f); + objectInstances[7].rotation = glm::vec3(45.0f, 0.0f, 0.0f); + objectInstances[7].scale = glm::vec3(0.5f); + + // Instance 9 - Below + objectInstances[8].position = glm::vec3(0.0f, -1.0f, -2.0f); + objectInstances[8].rotation = glm::vec3(-30.0f, 0.0f, 0.0f); + objectInstances[8].scale = glm::vec3(0.5f); + + // Instance 10 - Far Back + objectInstances[9].position = glm::vec3(0.0f, 0.5f, -5.0f); + objectInstances[9].rotation = glm::vec3(0.0f, 180.0f, 0.0f); + objectInstances[9].scale = glm::vec3(1.2f); +} +---- + +This function creates ten instances of our model, each with a unique position, rotation, and scale. This allows us to create a more interesting scene with varied object placements. + +=== Rendering Multiple Objects + +Now that we have our object instances set up, we need to render them. Here's how we can modify our rendering loop to handle multiple objects: + +[source,cpp] +---- +void drawFrame() { + // ... (standard Vulkan frame setup) + + // Begin command buffer recording + commandBuffer.begin({}); + + // Transition image layout for rendering + transition_image_layout( + imageIndex, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eTopOfPipe, + vk::PipelineStageFlagBits2::eColorAttachmentOutput + ); + + // Set up rendering attachments + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + + // Bind pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline); + + // Set viewport and scissor + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Bind descriptor set with uniform buffer and textures + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Update view and projection in uniform buffer + UniformBufferObject ubo{}; + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted + + // Copy to uniform buffer + memcpy(uniformBuffersMapped[currentFrame], &ubo, sizeof(ubo)); + + // Render each object instance + for (size_t i = 0; i < objectInstances.size(); i++) { + const auto& instance = objectInstances[i]; + + // Create model matrix for this instance + glm::mat4 modelMatrix = glm::mat4(1.0f); + modelMatrix = glm::translate(modelMatrix, instance.position); + modelMatrix = glm::rotate(modelMatrix, glm::radians(instance.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(instance.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(instance.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + modelMatrix = glm::scale(modelMatrix, instance.scale); + + // Render all nodes in the model + renderNode(commandBuffer, model.nodes, modelMatrix); + } + + // End dynamic rendering + commandBuffer.endRendering(); + + // Transition image layout for presentation + transition_image_layout( + imageIndex, + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe + ); + + // End command buffer recording + commandBuffer.end(); + + // ... (submit command buffer and present) +} + +// Helper function to recursively render all nodes in the model +void renderNode(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // If this node has a mesh, render it + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + + // Set up push constants for material properties + PushConstantBlock pushConstants{}; + + if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; + } else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + } + + // Update model matrix in push constants + commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind vertex and index buffers + commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Draw the mesh + commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); + } + + // Recursively render children + if (!node->children.empty()) { + renderNode(commandBuffer, node->children, nodeMatrix); + } + } +} +---- + +This rendering approach leverages our model system to efficiently render multiple instances of a model: + +1. It uses the scene graph structure to handle complex models with multiple parts +2. It properly handles parent-child relationships and hierarchical transformations +3. It applies material properties to each mesh using push constants +4. It supports animations through the node transformation system + +While this approach is more sophisticated than a simple flat list of objects, it does have some limitations: + +1. It still requires a separate draw call for each mesh in each instance, which can be inefficient for large numbers of objects +2. It doesn't implement any culling or batching optimizations +3. For very large scenes, additional spatial partitioning would be beneficial + +=== Advanced Techniques: Hardware Instancing + +For more efficient rendering of many similar objects, we can use hardware instancing. This allows us to draw multiple instances of the same model with a single draw call: + +[source,cpp] +---- +// Instance data for GPU instancing +struct InstanceData { + glm::mat4 model; // Model matrix for this instance +}; + +// Create buffers to hold instance data for each node with a mesh +std::vector instanceBuffers; +std::vector instanceBufferMemories; +std::vector instanceBuffersMapped; + +void setupInstanceBuffers() { + // Create an instance buffer for each node with a mesh + for (auto node : model.linearNodes) { + if (node->mesh.vertices.empty() || node->mesh.indices.empty()) { + continue; + } + + // Calculate buffer size + vk::DeviceSize bufferSize = sizeof(InstanceData) * objectInstances.size(); + + // Create the buffer + vk::raii::Buffer instanceBuffer = nullptr; + vk::raii::DeviceMemory instanceBufferMemory = nullptr; + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + instanceBuffer, + instanceBufferMemory + ); + + // Map the buffer memory + void* instanceBufferMapped = device.mapMemory(instanceBufferMemory, 0, bufferSize, {}); + + // Store buffer and memory + instanceBuffers.push_back(instanceBuffer); + instanceBufferMemories.push_back(instanceBufferMemory); + instanceBuffersMapped.push_back(instanceBufferMapped); + + // Set the instance buffer index for this node + node->instanceBufferIndex = static_cast(instanceBuffers.size() - 1); + } + + // Update all instance buffers + updateInstanceBuffers(); +} + +void updateInstanceBuffers() { + // For each node with an instance buffer + for (auto node : model.linearNodes) { + if (node->instanceBufferIndex < 0) { + continue; + } + + // Prepare instance data for this node + std::vector instanceData(objectInstances.size()); + for (size_t i = 0; i < objectInstances.size(); i++) { + // Create model matrix for this instance + glm::mat4 modelMatrix = glm::mat4(1.0f); + modelMatrix = glm::translate(modelMatrix, objectInstances[i].position); + modelMatrix = glm::rotate(modelMatrix, glm::radians(objectInstances[i].rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(objectInstances[i].rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(objectInstances[i].rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + modelMatrix = glm::scale(modelMatrix, objectInstances[i].scale); + + // Combine with node's local matrix + instanceData[i].model = modelMatrix * node->getLocalMatrix(); + } + + // Copy to instance buffer + memcpy(instanceBuffersMapped[node->instanceBufferIndex], instanceData.data(), sizeof(InstanceData) * instanceData.size()); + } +} + +// Modify vertex input state to include instance data +vk::PipelineVertexInputStateCreateInfo vertexInputInfo{}; +// ... (standard vertex input setup) + +// Add instance data bindings and attributes +vk::VertexInputBindingDescription instanceBindingDescription{}; +instanceBindingDescription.binding = 1; // Use binding point 1 for instance data +instanceBindingDescription.stride = sizeof(InstanceData); +instanceBindingDescription.inputRate = vk::VertexInputRate::eInstance; // Advance per instance + +// Four attributes for the 4x4 matrix (one per row) +std::array instanceAttributeDescriptions{}; +for (uint32_t i = 0; i < 4; i++) { + instanceAttributeDescriptions[i].binding = 1; + instanceAttributeDescriptions[i].location = 4 + i; // Start after vertex attributes + instanceAttributeDescriptions[i].format = vk::Format::eR32G32B32A32Sfloat; + instanceAttributeDescriptions[i].offset = sizeof(float) * 4 * i; +} + +// Combine vertex and instance bindings/attributes +std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription +}; + +std::vector attributeDescriptions; +// Add vertex attributes +for (const auto& attr : vertexAttributeDescriptions) { + attributeDescriptions.push_back(attr); +} +// Add instance attributes +for (const auto& attr : instanceAttributeDescriptions) { + attributeDescriptions.push_back(attr); +} + +// Update vertex input info +vertexInputInfo.vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()); +vertexInputInfo.pVertexBindingDescriptions = bindingDescriptions.data(); +vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); +vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); +---- + +With hardware instancing set up, we can modify our rendering loop to draw all instances in a single call: + +[source,cpp] +---- +void drawFrame() { + // ... (standard Vulkan frame setup) + + // Begin command buffer recording + commandBuffer.begin({}); + + // Transition image layout for rendering + transition_image_layout( + imageIndex, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eTopOfPipe, + vk::PipelineStageFlagBits2::eColorAttachmentOutput + ); + + // Set up rendering attachments + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + + // Bind pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline); + + // Set viewport and scissor + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Update view and projection in uniform buffer + UniformBufferObject ubo{}; + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted + + // Copy to uniform buffer + memcpy(uniformBuffersMapped[currentFrame], &ubo, sizeof(ubo)); + + // Bind descriptor set + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Render all nodes in the model with instancing + renderNodeInstanced(commandBuffer, model.nodes); + + // End dynamic rendering + commandBuffer.endRendering(); + + // Transition image layout for presentation + transition_image_layout( + imageIndex, + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe + ); + + // End command buffer recording + commandBuffer.end(); + + // ... (submit command buffer and present) +} + +// Helper function to recursively render all nodes in the model with instancing +void renderNodeInstanced(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes) { + for (const auto node : nodes) { + // If this node has a mesh and an instance buffer, render it + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0 && + node->instanceBufferIndex >= 0) { + + // Set up push constants for material properties + PushConstantBlock pushConstants{}; + + if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; + } else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + } + + // Update push constants + commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind vertex and instance buffers + vk::Buffer vertexBuffers[] = {*vertexBuffers[node->vertexBufferIndex], *instanceBuffers[node->instanceBufferIndex]}; + vk::DeviceSize offsets[] = {0, 0}; + commandBuffer.bindVertexBuffers(0, 2, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Draw all instances of this mesh in a single call + commandBuffer.drawIndexed( + static_cast(node->mesh.indices.size()), + static_cast(objectInstances.size()), // Instance count + 0, 0, 0 + ); + } + + // Recursively render children + if (!node->children.empty()) { + renderNodeInstanced(commandBuffer, node->children); + } + } +} +---- + +This approach is much more efficient for rendering large numbers of similar objects, as it reduces the number of draw calls and uniform buffer updates. + +=== Vertex Shader Modifications for Instancing + +To support hardware instancing, we need to modify our vertex shader to use the instance data: + +[source,glsl] +---- +#version 450 + +// Vertex attributes +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec3 inColor; +layout(location = 3) in vec2 inTexCoord; + +// Instance attributes (model matrix, one row per attribute) +layout(location = 4) in vec4 instanceModelRow0; +layout(location = 5) in vec4 instanceModelRow1; +layout(location = 6) in vec4 instanceModelRow2; +layout(location = 7) in vec4 instanceModelRow3; + +// Uniform buffer for view and projection matrices +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; + + // PBR parameters (not used in this shader but included for compatibility) + vec4 lightPositions[4]; + vec4 lightColors[4]; + vec4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +} ubo; + +// Output to fragment shader +layout(location = 0) out vec3 fragPosition; +layout(location = 1) out vec3 fragNormal; +layout(location = 2) out vec2 fragTexCoord; +layout(location = 3) out vec3 fragColor; + +void main() { + // Reconstruct model matrix from instance attributes + mat4 instanceModel = mat4( + instanceModelRow0, + instanceModelRow1, + instanceModelRow2, + instanceModelRow3 + ); + + // Calculate world position + vec4 worldPos = instanceModel * vec4(inPosition, 1.0); + + // Output position in clip space + gl_Position = ubo.proj * ubo.view * worldPos; + + // Pass data to fragment shader + fragPosition = worldPos.xyz; + fragNormal = mat3(instanceModel) * inNormal; // This is simplified; should use normal matrix + fragTexCoord = inTexCoord; + fragColor = inColor; +} +---- + +=== Beyond Basic Instancing: Material Variations + +So far, we've focused on positioning multiple instances of the same model with the same material. In a real application, you might want to vary the materials as well: + +[source,cpp] +---- +// Create materials with variations for each instance +void createMaterialVariations() { + // Resize the materials vector to hold one material per instance + model.materials.resize(objectInstances.size()); + + for (size_t i = 0; i < objectInstances.size(); i++) { + // Get reference to this instance's material + Material& material = model.materials[i]; + + // Vary materials based on position or other factors + float distanceFromCenter = glm::length(objectInstances[i].position); + float angle = atan2(objectInstances[i].position.z, objectInstances[i].position.x); + + // Vary color based on angle + float hue = (angle + glm::pi()) / (2.0f * glm::pi()); + glm::vec3 color = hsvToRgb(glm::vec3(hue, 0.7f, 0.9f)); + material.baseColorFactor = glm::vec4(color, 1.0f); + + // Vary metallic/roughness based on distance + material.metallicFactor = glm::clamp(distanceFromCenter / 5.0f, 0.0f, 1.0f); + material.roughnessFactor = glm::clamp(1.0f - distanceFromCenter / 5.0f, 0.1f, 0.9f); + + // Vary emissive strength for some objects + material.emissiveFactor = (i % 3 == 0) ? glm::vec3(1.0f) : glm::vec3(0.0f); // Every third object glows + } + + // Update material indices for all nodes + for (auto node : model.linearNodes) { + // For demonstration, we'll assign materials based on node index + // In a real application, you might use more sophisticated logic + if (!node->mesh.vertices.empty()) { + size_t materialIndex = node->index % objectInstances.size(); + node->mesh.materialIndex = static_cast(materialIndex); + } + } +} + +// Helper function to convert HSV to RGB +glm::vec3 hsvToRgb(glm::vec3 hsv) { + float h = hsv.x; + float s = hsv.y; + float v = hsv.z; + + float r, g, b; + + int i = floor(h * 6); + float f = h * 6 - i; + float p = v * (1 - s); + float q = v * (1 - f * s); + float t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + return glm::vec3(r, g, b); +} + +// To use these material variations, call createMaterialVariations() after loading the model +// The renderNode() and renderNodeInstanced() methods will automatically use the assigned materials +---- + +This approach allows for much more visual variety in your scene, even when using the same base model for all instances. + +=== Conclusion and Next Steps + +In this chapter, we've explored how to manage and render multiple objects in a 3D scene. We've covered: + +* Different approaches to organizing multiple objects +* Performance considerations for multi-object rendering +* Basic implementation of object instances +* Advanced techniques like hardware instancing +* Material variations for visual diversity + +These techniques form the foundation for creating complex, visually rich 3D scenes. In the next chapter, we'll build upon this foundation to implement a complete scene rendering system that integrates all the components we've developed so far. + +link:05_pbr_rendering.adoc[Previous: Understanding Physically Based Rendering] | link:07_scene_rendering.adoc[Next: Rendering the Scene] diff --git a/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc new file mode 100644 index 00000000..3de386bc --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc @@ -0,0 +1,628 @@ +::pp: {plus}{plus} + += Loading Models: Rendering the Scene +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Rendering the Scene + +=== Introduction to Scene Rendering + +Scene rendering is the process of transforming a 3D scene description into a 2D image that can be displayed on screen. In our engine, this involves traversing the scene graph, applying transformations, setting material properties, and issuing draw commands to the GPU. + +The scene rendering process is a critical part of the rendering pipeline, as it's where all the components we've built so far come together: + +* The model system provides the scene graph structure and mesh data +* The material system defines the appearance of objects +* The camera system determines the viewpoint +* The lighting system illuminates the scene + +In this chapter, we'll explore how these components work together to render a complete scene. + +=== Scene Graph Traversal + +A scene graph is a hierarchical tree structure that organizes objects in a scene. Each node in the tree can have a transformation (position, rotation, scale) and may contain a mesh to render. Nodes can also have child nodes, which inherit their parent's transformation. + +To render a scene graph, we need to traverse it in a depth-first manner, calculating the global transformation matrix for each node and rendering any meshes we encounter: + +[source,cpp] +---- +void renderScene(const vk::raii::CommandBuffer& commandBuffer, Model& model, const glm::mat4& viewMatrix, const glm::mat4& projectionMatrix) { + // Start traversal from the root nodes with an identity matrix + glm::mat4 rootMatrix = glm::mat4(1.0f); + renderNode(commandBuffer, model.nodes, rootMatrix); +} +---- + +The `renderNode` function is the heart of our scene rendering system. It recursively traverses the scene graph, calculating the global transformation matrix for each node and rendering any meshes it contains: + +[source,cpp] +---- +void renderNode(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // If this node has a mesh, render it + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + + // Set up push constants for material properties + PushConstantBlock pushConstants{}; + + if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; + } else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + } + + // Update model matrix in push constants + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind vertex and index buffers + commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Draw the mesh + commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); + } + + // Recursively render children + if (!node->children.empty()) { + renderNode(commandBuffer, node->children, nodeMatrix); + } + } +} +---- + +This traversal approach ensures that: + +1. Each node's transformation is correctly combined with its parent's transformation +2. Child nodes are rendered with the correct global transformation +3. The scene graph hierarchy is preserved during rendering + +=== Understanding the Rendering Process + +Let's break down the rendering process in more detail: + +==== Transformation Calculation + +The first step in rendering a node is calculating its global transformation matrix: + +[source,cpp] +---- +// Calculate global matrix for this node +glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); +---- + +This combines the node's local transformation (position, rotation, scale) with its parent's global transformation. The result is a matrix that transforms from the node's local space to world space. + +The `getLocalMatrix` method (defined in the `Node` class) combines the node's translation, rotation, and scale properties: + +[source,cpp] +---- +glm::mat4 getLocalMatrix() { + return glm::translate(glm::mat4(1.0f), translation) * + glm::toMat4(rotation) * + glm::scale(glm::mat4(1.0f), scale) * + matrix; +} +---- + +==== Material Setup + +If the node has a mesh, we need to set up its material properties before rendering: + +[source,cpp] +---- +// Set up push constants for material properties +PushConstantBlock pushConstants{}; + +if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; +} else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; +} + +// Update push constants +commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); +---- + +This code: + +1. Retrieves the material associated with the mesh +2. Sets up push constants with the material properties +3. Passes these properties to the fragment shader using push constants + +The material properties include: + +* Base color factor (albedo) +* Metallic factor +* Roughness factor +* Texture set indices for various material maps (base color, metallic-roughness, normal, occlusion, emissive) + +==== Mesh Rendering + +Once the transformation and material are set up, we can render the mesh: + +[source,cpp] +---- +// Bind vertex and index buffers +commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); +commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + +// Draw the mesh +commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); +---- + +This code: + +1. Binds the vertex buffer containing the mesh's vertices +2. Binds the index buffer containing the mesh's indices +3. Issues a draw command to render the mesh + +==== Recursive Traversal + +After rendering the current node, we recursively traverse its children: + +[source,cpp] +---- +// Recursively render children +if (!node->children.empty()) { + renderNode(commandBuffer, node->children, nodeMatrix); +} +---- + +This ensures that all nodes in the scene graph are visited and rendered with the correct transformations. + +=== Integrating Scene Rendering in the Main Loop + +To use our scene rendering system in the main rendering loop, we need to set up the necessary Vulkan state and call the `renderScene` function: + +[source,cpp] +---- +void drawFrame() { + // ... (standard Vulkan frame setup) + + // Begin command buffer recording + commandBuffer.begin({}); + + // Transition image layout for rendering + transition_image_layout( + imageIndex, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eTopOfPipe, + vk::PipelineStageFlagBits2::eColorAttachmentOutput + ); + + // Set up rendering attachments + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + + // Bind pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline); + + // Set viewport and scissor + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Bind descriptor set with uniform buffer and textures + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Update view and projection in uniform buffer + UniformBufferObject ubo{}; + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted + + // Copy to uniform buffer + memcpy(uniformBuffersMapped[currentFrame], &ubo, sizeof(ubo)); + + // Render the scene + renderScene(commandBuffer, model, ubo.view, ubo.proj); + + // End dynamic rendering + commandBuffer.endRendering(); + + // Transition image layout for presentation + transition_image_layout( + imageIndex, + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe + ); + + // End command buffer recording + commandBuffer.end(); + + // ... (submit command buffer and present) +} +---- + +This code: + +1. Sets up the Vulkan rendering state (command buffer, image transitions, rendering attachments) +2. Binds the graphics pipeline and descriptor sets +3. Updates the view and projection matrices in the uniform buffer +4. Calls `renderScene` to render the entire scene +5. Finalizes the rendering and presents the result + +=== Performance Considerations + +Rendering a complex scene can be performance-intensive. Here are some techniques to optimize scene rendering: + +==== Frustum Culling + +Frustum culling is the process of skipping the rendering of objects that are outside the camera's view frustum. This can significantly improve performance by reducing the number of draw calls: + +[source,cpp] +---- +bool isNodeVisible(const Node* node, const glm::mat4& viewProjection) { + // Calculate the node's bounding sphere in world space + glm::vec3 center = glm::vec3(node->getGlobalMatrix() * glm::vec4(node->boundingSphere.center, 1.0f)); + float radius = node->boundingSphere.radius * glm::length(glm::vec3(node->getGlobalMatrix()[0])); // Scale radius by the largest scale factor + + // Check if the bounding sphere is visible in the view frustum + for (int i = 0; i < 6; i++) { + // Extract frustum planes from the view-projection matrix + glm::vec4 plane = getFrustumPlane(viewProjection, i); + + // Calculate the signed distance from the sphere center to the plane + float distance = glm::dot(glm::vec4(center, 1.0f), plane); + + // If the sphere is completely behind the plane, it's not visible + if (distance < -radius) { + return false; + } + } + + return true; +} + +void renderNodeWithCulling(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix, const glm::mat4& viewProjection) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Check if the node is visible + if (isNodeVisible(node, viewProjection)) { + // Render the node (same as before) + // ... + + // Recursively render children + if (!node->children.empty()) { + renderNodeWithCulling(commandBuffer, node->children, nodeMatrix, viewProjection); + } + } + } +} +---- + +==== Level of Detail (LOD) + +Level of Detail (LOD) involves using simpler versions of models for objects that are far from the camera: + +[source,cpp] +---- +void renderNodeWithLOD(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix, const glm::vec3& cameraPosition) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Calculate distance to camera + glm::vec3 nodePosition = glm::vec3(nodeMatrix[3]); + float distanceToCamera = glm::distance(nodePosition, cameraPosition); + + // Select LOD level based on distance + int lodLevel = 0; + if (distanceToCamera > 50.0f) { + lodLevel = 2; // Low detail + } else if (distanceToCamera > 20.0f) { + lodLevel = 1; // Medium detail + } + + // Render the node with the selected LOD level + // ... + + // Recursively render children + if (!node->children.empty()) { + renderNodeWithLOD(commandBuffer, node->children, nodeMatrix, cameraPosition); + } + } +} +---- + +==== Occlusion Culling + +Occlusion culling involves skipping the rendering of objects that are hidden behind other objects: + +[source,cpp] +---- +void renderNodeWithOcclusion(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { + // Sort nodes by distance to camera (front to back) + std::vector> sortedNodes; + for (const auto node : nodes) { + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + glm::vec3 nodePosition = glm::vec3(nodeMatrix[3]); + float distanceToCamera = glm::length(nodePosition - cameraPosition); + sortedNodes.push_back({node, distanceToCamera}); + } + std::sort(sortedNodes.begin(), sortedNodes.end(), [](const auto& a, const auto& b) { + return a.second < b.second; + }); + + // Render nodes from front to back + for (const auto& [node, distance] : sortedNodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Begin occlusion query + vk::QueryPool occlusionQueryPool = createOcclusionQueryPool(); + commandBuffer.beginQuery(occlusionQueryPool, 0, {}); + + // Render the node's bounding box with depth write but no color write + renderBoundingBox(commandBuffer, node, nodeMatrix); + + // End occlusion query + commandBuffer.endQuery(occlusionQueryPool, 0); + + // Check if the node is visible + uint64_t occlusionResult = getOcclusionQueryResult(occlusionQueryPool); + if (occlusionResult > 0) { + // Node is visible, render it + // ... + + // Recursively render children + if (!node->children.empty()) { + renderNodeWithOcclusion(commandBuffer, node->children, nodeMatrix); + } + } + } +} +---- + +==== Instanced Rendering + +For scenes with many identical objects, instanced rendering can significantly improve performance: + +[source,cpp] +---- +void renderInstanced(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const std::vector& instanceMatrices) { + for (const auto node : nodes) { + // If this node has a mesh, render it with instancing + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + + // Set up material properties (same as before) + // ... + + // Bind vertex and index buffers + commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Create and bind instance buffer + vk::raii::Buffer instanceBuffer = createInstanceBuffer(instanceMatrices); + commandBuffer.bindVertexBuffers(1, *instanceBuffer, {0}); + + // Draw the mesh with instancing + commandBuffer.drawIndexedInstanced( + static_cast(node->mesh.indices.size()), + static_cast(instanceMatrices.size()), + 0, 0, 0 + ); + } + + // Recursively render children + if (!node->children.empty()) { + renderInstanced(commandBuffer, node->children, instanceMatrices); + } + } +} +---- + +=== Advanced Scene Rendering Techniques + +Beyond basic scene rendering, there are several advanced techniques that can enhance the visual quality and performance of your renderer: + +==== Hierarchical Culling + +Hierarchical culling involves using the scene graph structure to accelerate culling operations: + +[source,cpp] +---- +bool isNodeAndChildrenVisible(const Node* node, const glm::mat4& viewProjection, const glm::mat4& parentMatrix) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Check if the node's bounding volume is visible + if (!isNodeVisible(node, viewProjection, nodeMatrix)) { + // If the node is not visible, none of its children are visible either + return false; + } + + // Node is visible, check if it has a mesh to render + bool hasVisibleContent = !node->mesh.vertices.empty() && !node->mesh.indices.empty(); + + // Recursively check children + for (const auto child : node->children) { + hasVisibleContent |= isNodeAndChildrenVisible(child, viewProjection, nodeMatrix); + } + + return hasVisibleContent; +} + +void renderNodeHierarchical(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix, const glm::mat4& viewProjection) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Check if the node and its children are visible + if (isNodeAndChildrenVisible(node, viewProjection, glm::mat4(1.0f))) { + // Render the node if it has a mesh + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + // Render the node (same as before) + // ... + } + + // Recursively render children + if (!node->children.empty()) { + renderNodeHierarchical(commandBuffer, node->children, nodeMatrix, viewProjection); + } + } + } +} +---- + +==== Deferred Rendering + +Deferred rendering separates the geometry and lighting passes, which can improve performance for scenes with many lights: + +[source,cpp] +---- +void renderSceneDeferred(const vk::raii::CommandBuffer& commandBuffer, Model& model) { + // Geometry pass: render scene to G-buffer + beginGeometryPass(commandBuffer); + renderNode(commandBuffer, model.nodes, glm::mat4(1.0f)); + endGeometryPass(commandBuffer); + + // Lighting pass: apply lighting to G-buffer + beginLightingPass(commandBuffer); + for (const auto& light : lights) { + renderLight(commandBuffer, light); + } + endLightingPass(commandBuffer); +} +---- + +==== Clustered Rendering + +Clustered rendering divides the view frustum into 3D cells to efficiently handle many lights: + +[source,cpp] +---- +void setupLightClusters() { + // Divide the view frustum into a 3D grid of clusters + const int clusterCountX = 16; + const int clusterCountY = 9; + const int clusterCountZ = 24; + + // Assign lights to clusters based on their position and radius + for (const auto& light : lights) { + for (int x = 0; x < clusterCountX; x++) { + for (int y = 0; y < clusterCountY; y++) { + for (int z = 0; z < clusterCountZ; z++) { + if (lightAffectsCluster(light, x, y, z)) { + lightClusters[x][y][z].push_back(light.index); + } + } + } + } + } + + // Upload light cluster data to GPU + updateLightClusterBuffer(); +} + +void renderSceneClustered(const vk::raii::CommandBuffer& commandBuffer, Model& model) { + // Bind light cluster buffer + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 1, + 1, + &lightClusterDescriptorSet, + 0, + nullptr + ); + + // Render scene normally + renderNode(commandBuffer, model.nodes, glm::mat4(1.0f)); +} +---- + +=== Conclusion + +In this chapter, we've explored the process of rendering a scene using a scene graph. We've seen how to traverse the scene graph, calculate transformations, apply materials, and render meshes. We've also discussed various optimization techniques to improve performance. + +The scene rendering system we've built is flexible and extensible, allowing for the rendering of complex scenes with multiple objects, materials, and lighting conditions. In the next chapter, we'll build on this foundation to implement animations, bringing our scenes to life. + +link:06_multiple_objects.adoc[Previous: Rendering Multiple Objects] | link:08_animations.adoc[Next: Updating Animations] diff --git a/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc b/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc new file mode 100644 index 00000000..c8580a6b --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc @@ -0,0 +1,1023 @@ +::pp: {plus}{plus} + += Loading Models: Updating Animations +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Understanding and Implementing Animations + +=== Introduction to 3D Animations + +Animation is a crucial aspect of modern 3D applications, bringing static models to life with movement and interactivity. In our engine, we've implemented a robust animation system that supports skeletal animations from glTF files. + +Animations in 3D graphics typically involve: + +* *Keyframes*: Specific points in time where the state of an object is explicitly defined +* *Interpolation*: The process of calculating intermediate states between keyframes +* *Channels*: Different properties that can be animated (position, rotation, scale) +* *Bones/Joints*: A hierarchical structure that defines how parts of a model move together + +glTF provides a standardized way to store and transfer animations, which our engine can load and play back. + +=== Animation Data Structures + +As we saw in the link:03_model_system.adoc[Model System chapter], our engine uses several structures to represent animations: + +[source,cpp] +---- +// Structure for animation keyframes +struct AnimationChannel { + enum PathType { TRANSLATION, ROTATION, SCALE }; + PathType path; + Node* node = nullptr; + uint32_t samplerIndex; +}; + +// Structure for animation interpolation +struct AnimationSampler { + enum InterpolationType { LINEAR, STEP, CUBICSPLINE }; + InterpolationType interpolation; + std::vector inputs; // Key frame timestamps + std::vector outputsVec4; // Key frame values (for rotations) + std::vector outputsVec3; // Key frame values (for translations and scales) +}; + +// Structure for animation +struct Animation { + std::string name; + std::vector samplers; + std::vector channels; + float start = std::numeric_limits::max(); + float end = std::numeric_limits::min(); + float currentTime = 0.0f; +}; +---- + +These structures work together to define how animations are stored and processed: + +* *Animation*: Contains multiple channels and samplers, representing a complete animation sequence +* *AnimationChannel*: Links a node in the scene graph to a specific animation property (translation, rotation, or scale) +* *AnimationSampler*: Defines how to interpolate between keyframes for a specific channel + +=== How Animation Playback Works + +The core of our animation system is the `updateAnimation` method in the Model class: + +[source,cpp] +---- +void Model::updateAnimation(uint32_t index, float deltaTime) { + if (animations.empty() || index >= animations.size()) { + return; + } + + Animation& animation = animations[index]; + animation.currentTime += deltaTime; + if (animation.currentTime > animation.end) { + animation.currentTime = animation.start; + } + + for (auto& channel : animation.channels) { + AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; + + // Find the current key frame + for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { + if (animation.currentTime >= sampler.inputs[i] && animation.currentTime <= sampler.inputs[i + 1]) { + float t = (animation.currentTime - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]); + + switch (channel.path) { + case AnimationChannel::TRANSLATION: { + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->translation = glm::mix(start, end, t); + break; + } + case AnimationChannel::ROTATION: { + glm::quat start = glm::quat(sampler.outputsVec4[i].w, sampler.outputsVec4[i].x, sampler.outputsVec4[i].y, sampler.outputsVec4[i].z); + glm::quat end = glm::quat(sampler.outputsVec4[i + 1].w, sampler.outputsVec4[i + 1].x, sampler.outputsVec4[i + 1].y, sampler.outputsVec4[i + 1].z); + channel.node->rotation = glm::slerp(start, end, t); + break; + } + case AnimationChannel::SCALE: { + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->scale = glm::mix(start, end, t); + break; + } + } + break; + } + } + } +} +---- + +This method: + +1. Updates the animation's current time based on the delta time +2. Loops the animation if it reaches the end +3. For each channel in the animation: + a. Finds the current keyframe based on the current time + b. Calculates the interpolation factor between the current and next keyframe + c. Interpolates between keyframe values based on the channel type (translation, rotation, or scale) + d. Updates the corresponding node's transformation + +=== Integrating Animation Updates in the Main Loop + +To animate our models, we need to update the animation state each frame: + +[source,cpp] +---- +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + + // Update animation time + static auto lastTime = std::chrono::high_resolution_clock::now(); + auto currentTime = std::chrono::high_resolution_clock::now(); + float deltaTime = std::chrono::duration(currentTime - lastTime).count(); + lastTime = currentTime; + + // Update model animations + animationTime += deltaTime; + if (!model.animations.empty()) { + model.updateAnimation(0, deltaTime); + } + + drawFrame(); + } + + device.waitIdle(); +} +---- + +This code: + +1. Calculates the time elapsed since the last frame (deltaTime) +2. Updates a global animation time counter (useful for custom animations) +3. Calls `updateAnimation` on the model if it has animations +4. Renders the frame with the updated animation state + +=== Advanced Animation Techniques + +While our basic animation system handles most common use cases, there are several advanced techniques you might want to implement: + +==== Animation Blending + +Animation blending is a technique that combines multiple animations to create smooth transitions or entirely new animations. This is essential for creating realistic character movement and responsive gameplay. + +===== Understanding Animation Blending + +At its core, animation blending works by interpolating between the transformations (position, rotation, scale) of corresponding bones or nodes in different animations. The key concepts include: + +* *Blend Factor*: A value between 0.0 and 1.0 that determines how much of each animation contributes to the final result +* *Blend Space*: A multidimensional space where animations are positioned based on parameters (like speed, direction) +* *Blend Trees*: Hierarchical structures that organize multiple blends into complex animation systems + +===== Types of Animation Blending + +There are several common types of animation blending: + +* *Linear Blending*: Simple interpolation between two animations (e.g., transitioning from walk to run) +* *Additive Blending*: One animation is added on top of another (e.g., adding a "wounded" limp to any movement animation) +* *Partial Blending*: Blending that affects only certain parts of the skeleton (e.g., aiming a weapon while walking) +* *Parametric Blending*: Blending multiple animations based on continuous parameters (e.g., direction + speed) + +===== Implementing Basic Animation Blending + +Here's how to implement a simple linear blend between two animations: + +[source,cpp] +---- +void blendAnimations(uint32_t fromIndex, uint32_t toIndex, float blendFactor) { + // Store original node transformations + std::vector originalTranslations; + std::vector originalRotations; + std::vector originalScales; + + for (auto node : model.linearNodes) { + originalTranslations.push_back(node->translation); + originalRotations.push_back(node->rotation); + originalScales.push_back(node->scale); + } + + // Apply first animation fully + model.updateAnimation(fromIndex, 0.0f); + + // Store intermediate transformations + std::vector fromTranslations; + std::vector fromRotations; + std::vector fromScales; + + for (auto node : model.linearNodes) { + fromTranslations.push_back(node->translation); + fromRotations.push_back(node->rotation); + fromScales.push_back(node->scale); + } + + // Restore original transformations + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = originalTranslations[i]; + model.linearNodes[i]->rotation = originalRotations[i]; + model.linearNodes[i]->scale = originalScales[i]; + } + + // Apply second animation fully + model.updateAnimation(toIndex, 0.0f); + + // Blend between the two animations + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = glm::mix(fromTranslations[i], model.linearNodes[i]->translation, blendFactor); + model.linearNodes[i]->rotation = glm::slerp(fromRotations[i], model.linearNodes[i]->rotation, blendFactor); + model.linearNodes[i]->scale = glm::mix(fromScales[i], model.linearNodes[i]->scale, blendFactor); + } +} +---- + +This implementation: + +1. Captures the original state of all nodes +2. Applies the first animation and stores its transformations +3. Restores the original state +4. Applies the second animation +5. Blends between the two animations using linear interpolation for positions and scales, and spherical interpolation for rotations + +===== Advanced Blending Techniques + +For more complex scenarios, we can implement more sophisticated blending: + +[source,cpp] +---- +// Multi-way blending with weights +void blendMultipleAnimations(const std::vector& animationIndices, + const std::vector& weights) { + if (animationIndices.empty() || weights.empty() || + animationIndices.size() != weights.size()) { + return; + } + + // Normalize weights + float totalWeight = 0.0f; + for (float weight : weights) { + totalWeight += weight; + } + + std::vector> allTranslations; + std::vector> allRotations; + std::vector> allScales; + + // Store original transformations + std::vector originalTranslations; + std::vector originalRotations; + std::vector originalScales; + + for (auto node : model.linearNodes) { + originalTranslations.push_back(node->translation); + originalRotations.push_back(node->rotation); + originalScales.push_back(node->scale); + } + + // Collect transformations from all animations + for (uint32_t animIndex : animationIndices) { + // Reset to original state + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = originalTranslations[i]; + model.linearNodes[i]->rotation = originalRotations[i]; + model.linearNodes[i]->scale = originalScales[i]; + } + + // Apply this animation + model.updateAnimation(animIndex, 0.0f); + + // Store transformations + std::vector translations; + std::vector rotations; + std::vector scales; + + for (auto node : model.linearNodes) { + translations.push_back(node->translation); + rotations.push_back(node->rotation); + scales.push_back(node->scale); + } + + allTranslations.push_back(translations); + allRotations.push_back(rotations); + allScales.push_back(scales); + } + + // Reset to original state + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = originalTranslations[i]; + model.linearNodes[i]->rotation = originalRotations[i]; + model.linearNodes[i]->scale = originalScales[i]; + } + + // Apply weighted blend + for (size_t nodeIdx = 0; nodeIdx < model.linearNodes.size(); nodeIdx++) { + glm::vec3 blendedTranslation(0.0f); + glm::quat blendedRotation(0.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 blendedScale(0.0f); + + // First pass for translations and scales + for (size_t animIdx = 0; animIdx < animationIndices.size(); animIdx++) { + float normalizedWeight = weights[animIdx] / totalWeight; + blendedTranslation += allTranslations[animIdx][nodeIdx] * normalizedWeight; + blendedScale += allScales[animIdx][nodeIdx] * normalizedWeight; + } + + // Special handling for quaternions (rotations) + // We use nlerp (normalized lerp) for multiple quaternions + for (size_t animIdx = 0; animIdx < animationIndices.size(); animIdx++) { + float normalizedWeight = weights[animIdx] / totalWeight; + if (animIdx == 0) { + blendedRotation = allRotations[animIdx][nodeIdx] * normalizedWeight; + } else { + // Ensure we're interpolating along the shortest path + if (glm::dot(blendedRotation, allRotations[animIdx][nodeIdx]) < 0) { + blendedRotation += -allRotations[animIdx][nodeIdx] * normalizedWeight; + } else { + blendedRotation += allRotations[animIdx][nodeIdx] * normalizedWeight; + } + } + } + + // Normalize the resulting quaternion + blendedRotation = glm::normalize(blendedRotation); + + // Apply the blended transformations + model.linearNodes[nodeIdx]->translation = blendedTranslation; + model.linearNodes[nodeIdx]->rotation = blendedRotation; + model.linearNodes[nodeIdx]->scale = blendedScale; + } +} +---- + +This more advanced implementation allows for blending between any number of animations with different weights, which is essential for complex animation systems like locomotion or facial expressions. + +===== Blend Spaces + +For character movement, blend spaces are particularly useful. A blend space is a 2D or 3D space where animations are positioned based on parameters like speed and direction: + +[source,cpp] +---- +// Simple 2D blend space for locomotion (direction + speed) +struct BlendSpaceAnimation { + uint32_t animationIndex; + float directionAngle; // In degrees, 0 = forward, 180 = backward + float speed; // In units/second +}; + +void updateLocomotionBlendSpace(float currentDirection, float currentSpeed) { + // Define our blend space animations + std::vector blendSpace = { + {0, 0.0f, 0.0f}, // Idle + {1, 0.0f, 1.0f}, // Walk Forward + {2, 0.0f, 3.0f}, // Run Forward + {3, 90.0f, 1.0f}, // Walk Right + {4, 90.0f, 3.0f}, // Run Right + {5, 180.0f, 1.0f}, // Walk Backward + {6, 180.0f, 3.0f}, // Run Backward + {7, 270.0f, 1.0f}, // Walk Left + {8, 270.0f, 3.0f} // Run Left + }; + + // Find the closest animations and their weights + std::vector animIndices; + std::vector weights; + + // Normalize direction to 0-360 range + currentDirection = fmod(currentDirection + 360.0f, 360.0f); + + // Find the 3 closest animations in the blend space + // This is a simplified approach - a real implementation would use triangulation + for (const auto& anim : blendSpace) { + float distDir = std::min(std::abs(currentDirection - anim.directionAngle), + 360.0f - std::abs(currentDirection - anim.directionAngle)); + float distSpeed = std::abs(currentSpeed - anim.speed); + + // Calculate distance in blend space (weighted combination of direction and speed) + float distance = std::sqrt(distDir * distDir * 0.01f + distSpeed * distSpeed); + + // Use inverse distance weighting + if (distance < 0.001f) { + // If we're very close to an exact animation, just use that one + animIndices = {anim.animationIndex}; + weights = {1.0f}; + break; + } + + float weight = 1.0f / (distance + 0.1f); // Add small epsilon to avoid division by zero + animIndices.push_back(anim.animationIndex); + weights.push_back(weight); + + // Limit to 3 closest animations for performance + if (animIndices.size() > 3) { + // Find the smallest weight + auto minIt = std::min_element(weights.begin(), weights.end()); + size_t minIdx = std::distance(weights.begin(), minIt); + + // Remove the animation with the smallest weight + animIndices.erase(animIndices.begin() + minIdx); + weights.erase(weights.begin() + minIdx); + } + } + + // Blend the selected animations + blendMultipleAnimations(animIndices, weights); +} +---- + +This blend space implementation allows for smooth transitions between different movement animations based on the character's current direction and speed. + +While animation blending gives us powerful tools to combine pre-created animations, sometimes we need to adapt animations to dynamic environments in real-time. For example, how do we make a character's hand precisely grab an object, or ensure feet properly plant on uneven terrain? This is where our next technique comes in. + +==== Inverse Kinematics (IK) + +Inverse Kinematics complements our animation system by allowing procedural adjustments to character poses. While the animation playback we implemented earlier uses Forward Kinematics (calculating positions from rotations), IK works in reverse - determining the joint rotations needed to achieve a specific end position. + +===== Forward vs. Inverse Kinematics + +To understand IK, it helps to contrast it with Forward Kinematics: + +* *Forward Kinematics (FK)*: Given joint angles, calculate the position of the end effector + - Straightforward to compute + - Predictable and stable + - Used in most animation playback + +* *Inverse Kinematics (IK)*: Given a desired end effector position, calculate the joint angles + - More complex to compute + - May have multiple solutions or no solution + - Essential for adaptive animations and interactions + +===== Common IK Applications + +Just as we use animation blending to create smooth transitions between predefined animations, we use IK to adapt those animations to dynamic environments. IK enhances our animation system in several key ways: + +* *Foot Placement*: Remember how our animations update node transformations? With IK, we can adjust those transformations to ensure feet properly contact uneven terrain, preventing the "floating feet" problem common in games +* *Hand Placement*: Similar to our blend space example where we interpolate between different animations, IK lets us precisely position a character's hands to grab objects at any position +* *Aiming*: We can use IK to orient a character's upper body toward a target while the lower body follows a different animation +* *Procedural Animation*: IK allows us to generate new animations on-the-fly based on environmental constraints +* *Ragdoll Physics*: When transitioning from animated to physics-driven movement (like when a character falls), IK helps create realistic physical responses + +===== IK Algorithms + +Just as we have different interpolation methods for animation keyframes (LINEAR, STEP, CUBICSPLINE in our AnimationSampler), we have different algorithms for solving IK problems: + +* *Analytical Methods*: For simple cases like two-bone chains (arm or leg), we can use closed-form mathematical solutions - similar to how we directly interpolate between two keyframes +* *Cyclic Coordinate Descent (CCD)*: An iterative approach that adjusts one joint at a time, working backward from the end effector +* *FABRIK (Forward And Backward Reaching Inverse Kinematics)*: Works by iteratively adjusting the entire chain, often converging faster than CCD +* *Jacobian Inverse*: Uses matrix operations to find optimal joint adjustments for complex chains + +===== Implementing Two-Bone IK + +The simplest and most common IK scenario involves a two-bone chain (like an arm or leg). Here's an implementation of the analytical two-bone IK solution: + +[source,cpp] +---- +// Two-bone IK solver +bool solveTwoBoneIK( + Node* rootNode, // The root joint (e.g., shoulder or hip) + Node* midNode, // The middle joint (e.g., elbow or knee) + Node* endNode, // The end effector (e.g., hand or foot) + const glm::vec3& targetPosition, // Target world position + const glm::vec3& hingeAxis, // Axis of rotation for the middle joint + float preferredAngle = 0.0f // Preferred angle for resolving ambiguity +) { + // Get the original global positions + glm::mat4 rootGlobal = rootNode->getGlobalMatrix(); + glm::mat4 midGlobal = midNode->getGlobalMatrix(); + glm::mat4 endGlobal = endNode->getGlobalMatrix(); + + glm::vec3 rootPos = glm::vec3(rootGlobal[3]); + glm::vec3 midPos = glm::vec3(midGlobal[3]); + glm::vec3 endPos = glm::vec3(endGlobal[3]); + + // Calculate bone lengths + float bone1Length = glm::length(midPos - rootPos); + float bone2Length = glm::length(endPos - midPos); + float totalLength = bone1Length + bone2Length; + + // Calculate the distance to the target + float targetDistance = glm::length(targetPosition - rootPos); + + // Check if the target is reachable + if (targetDistance > totalLength) { + // Target is too far - stretch as far as possible + glm::vec3 direction = glm::normalize(targetPosition - rootPos); + + // Set mid node position + glm::vec3 newMidPos = rootPos + direction * bone1Length; + + // Convert to local space and update node + glm::mat4 rootInv = glm::inverse(rootGlobal); + glm::vec3 localMidPos = glm::vec3(rootInv * glm::vec4(newMidPos, 1.0f)); + midNode->translation = localMidPos; + + // Update mid global matrix after changes + midGlobal = midNode->getGlobalMatrix(); + + // Set end node position + glm::vec3 newEndPos = newMidPos + direction * bone2Length; + + // Convert to local space and update node + glm::mat4 midInv = glm::inverse(midGlobal); + glm::vec3 localEndPos = glm::vec3(midInv * glm::vec4(newEndPos, 1.0f)); + endNode->translation = localEndPos; + + return false; // Target not fully reached + } + + // Target is reachable - apply cosine law to find the angles + float a = bone1Length; + float b = targetDistance; + float c = bone2Length; + + // Calculate the angle between the first bone and the target direction + float cosAngle1 = (b*b + a*a - c*c) / (2*b*a); + cosAngle1 = glm::clamp(cosAngle1, -1.0f, 1.0f); // Avoid numerical errors + float angle1 = acos(cosAngle1); + + // Calculate the direction to the target + glm::vec3 targetDir = glm::normalize(targetPosition - rootPos); + + // Create a rotation that aligns the x-axis with the target direction + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 rotAxis = glm::cross(xAxis, targetDir); + + if (glm::length(rotAxis) < 0.001f) { + // Target is along the x-axis, use the up vector + rotAxis = glm::vec3(0.0f, 1.0f, 0.0f); + } else { + rotAxis = glm::normalize(rotAxis); + } + + float rotAngle = acos(glm::dot(xAxis, targetDir)); + glm::quat targetRot = glm::angleAxis(rotAngle, rotAxis); + + // Create a rotation around the target direction by the preferred angle + glm::quat prefRot = glm::angleAxis(preferredAngle, targetDir); + + // Combine rotations + glm::quat finalRot = prefRot * targetRot * glm::angleAxis(angle1, hingeAxis); + + // Apply the rotation to the root node + rootNode->rotation = finalRot; + + // Update the mid node's global matrix after root changes + midGlobal = midNode->getGlobalMatrix(); + midPos = glm::vec3(midGlobal[3]); + + // Calculate the angle for the middle joint + float cosAngle2 = (a*a + c*c - b*b) / (2*a*c); + cosAngle2 = glm::clamp(cosAngle2, -1.0f, 1.0f); // Avoid numerical errors + float angle2 = acos(cosAngle2); + + // The middle joint bends in the opposite direction (PI - angle2) + glm::quat midRot = glm::angleAxis(glm::pi() - angle2, hingeAxis); + midNode->rotation = midRot; + + return true; // Target reached +} +---- + +This implementation: + +1. Calculates the positions and lengths of the bones +2. Checks if the target is reachable +3. Uses the law of cosines to calculate the necessary angles +4. Applies rotations to the joints to reach the target position + +===== Implementing CCD (Cyclic Coordinate Descent) + +For chains with more than two bones, CCD is a popular iterative approach: + +[source,cpp] +---- +// CCD IK solver +void solveCCDIK( + std::vector chain, // Joint chain from root to end effector + const glm::vec3& targetPosition, // Target world position + int maxIterations = 10, // Maximum iterations + float threshold = 0.01f // Distance threshold for success +) { + if (chain.size() < 2) return; + + // Get the end effector + Node* endEffector = chain.back(); + + for (int iteration = 0; iteration < maxIterations; iteration++) { + // Get current end effector position + glm::vec3 endPos = glm::vec3(endEffector->getGlobalMatrix()[3]); + + // Check if we're close enough to the target + if (glm::distance(endPos, targetPosition) < threshold) { + return; // Success + } + + // Work backwards from the second-to-last joint to the root + for (int i = chain.size() - 2; i >= 0; i--) { + Node* currentJoint = chain[i]; + + // Get joint position in world space + glm::mat4 jointGlobal = currentJoint->getGlobalMatrix(); + glm::vec3 jointPos = glm::vec3(jointGlobal[3]); + + // Get updated end effector position + endPos = glm::vec3(endEffector->getGlobalMatrix()[3]); + + // Calculate vectors from joint to end effector and target + glm::vec3 toEnd = glm::normalize(endPos - jointPos); + glm::vec3 toTarget = glm::normalize(targetPosition - jointPos); + + // Calculate rotation to align the vectors + float cosAngle = glm::dot(toEnd, toTarget); + cosAngle = glm::clamp(cosAngle, -1.0f, 1.0f); + + float angle = acos(cosAngle); + + // If the angle is small enough, skip this joint + if (angle < 0.01f) continue; + + // Calculate rotation axis + glm::vec3 rotAxis = glm::cross(toEnd, toTarget); + + // Handle the case where vectors are parallel + if (glm::length(rotAxis) < 0.001f) { + // Find an arbitrary perpendicular axis + glm::vec3 tempAxis(0.0f, 1.0f, 0.0f); + if (abs(glm::dot(toEnd, tempAxis)) > 0.9f) { + tempAxis = glm::vec3(1.0f, 0.0f, 0.0f); + } + rotAxis = glm::cross(toEnd, tempAxis); + } + + rotAxis = glm::normalize(rotAxis); + + // Create rotation quaternion + glm::quat rotation = glm::angleAxis(angle, rotAxis); + + // Apply rotation to the joint + currentJoint->rotation = rotation * currentJoint->rotation; + + // Check if we're close enough after this adjustment + endPos = glm::vec3(endEffector->getGlobalMatrix()[3]); + if (glm::distance(endPos, targetPosition) < threshold) { + return; // Success + } + } + } +} +---- + +This CCD implementation: + +1. Iteratively processes each joint from the end effector toward the root +2. For each joint, calculates the rotation needed to bring the end effector closer to the target +3. Applies the rotation and continues to the next joint +4. Repeats until the target is reached or the maximum iterations are exhausted + +===== Implementing FABRIK (Forward And Backward Reaching IK) + +FABRIK is another popular IK algorithm that often converges faster than CCD: + +[source,cpp] +---- +// FABRIK IK solver +void solveFABRIK( + std::vector chain, // Joint chain from root to end effector + const glm::vec3& targetPosition, // Target world position + bool constrainRoot = true, // Whether to keep the root fixed + int maxIterations = 10, // Maximum iterations + float threshold = 0.01f // Distance threshold for success +) { + if (chain.size() < 2) return; + + // Store original positions and bone lengths + std::vector positions; + std::vector lengths; + glm::vec3 rootOriginalPos; + + // Initialize positions and calculate lengths + for (size_t i = 0; i < chain.size(); i++) { + glm::vec3 pos = glm::vec3(chain[i]->getGlobalMatrix()[3]); + positions.push_back(pos); + + if (i > 0) { + lengths.push_back(glm::distance(positions[i], positions[i-1])); + } + } + + rootOriginalPos = positions[0]; + + // Check if the target is reachable + float totalLength = 0.0f; + for (float length : lengths) { + totalLength += length; + } + + glm::vec3 rootToTarget = targetPosition - positions[0]; + float targetDistance = glm::length(rootToTarget); + + if (targetDistance > totalLength) { + // Target is unreachable - stretch the chain + glm::vec3 direction = glm::normalize(rootToTarget); + + // Set all joints along the line to the target + positions[0] = constrainRoot ? rootOriginalPos : positions[0]; + + for (size_t i = 1; i < chain.size(); i++) { + positions[i] = positions[i-1] + direction * lengths[i-1]; + } + } else { + // Target is reachable - apply FABRIK + for (int iteration = 0; iteration < maxIterations; iteration++) { + // Check if we're already close enough + if (glm::distance(positions.back(), targetPosition) < threshold) { + break; + } + + // BACKWARD PASS: Set the end effector to the target and work backwards + positions.back() = targetPosition; + + for (int i = chain.size() - 2; i >= 0; i--) { + // Get the direction from this joint to the next + glm::vec3 direction = glm::normalize(positions[i] - positions[i+1]); + + // Set the position of this joint + positions[i] = positions[i+1] + direction * lengths[i]; + } + + // FORWARD PASS: Fix the root and work forwards + if (constrainRoot) { + positions[0] = rootOriginalPos; + } + + for (size_t i = 0; i < chain.size() - 1; i++) { + // Get the direction from this joint to the next + glm::vec3 direction = glm::normalize(positions[i+1] - positions[i]); + + // Set the position of the next joint + positions[i+1] = positions[i] + direction * lengths[i]; + } + + // Check if we're close enough after this iteration + if (glm::distance(positions.back(), targetPosition) < threshold) { + break; + } + } + } + + // Apply the new positions to the joints by calculating rotations + for (size_t i = 0; i < chain.size() - 1; i++) { + Node* currentJoint = chain[i]; + + // Calculate the original direction in local space + glm::mat4 parentGlobal = i > 0 ? chain[i-1]->getGlobalMatrix() : glm::mat4(1.0f); + glm::mat4 localToGlobal = currentJoint->getGlobalMatrix() * glm::inverse(parentGlobal); + glm::vec3 originalDir = glm::normalize(glm::vec3(localToGlobal * glm::vec4(1.0f, 0.0f, 0.0f, 0.0f))); + + // Calculate the new direction + glm::vec3 newDir = glm::normalize(positions[i+1] - positions[i]); + + // Calculate the rotation to align the directions + float cosAngle = glm::dot(originalDir, newDir); + cosAngle = glm::clamp(cosAngle, -1.0f, 1.0f); + + float angle = acos(cosAngle); + + // If the angle is small, skip this joint + if (angle < 0.01f) continue; + + // Calculate rotation axis + glm::vec3 rotAxis = glm::cross(originalDir, newDir); + + // Handle the case where vectors are parallel + if (glm::length(rotAxis) < 0.001f) { + // Find an arbitrary perpendicular axis + glm::vec3 tempAxis(0.0f, 1.0f, 0.0f); + if (abs(glm::dot(originalDir, tempAxis)) > 0.9f) { + tempAxis = glm::vec3(1.0f, 0.0f, 0.0f); + } + rotAxis = glm::cross(originalDir, tempAxis); + } + + rotAxis = glm::normalize(rotAxis); + + // Create rotation quaternion + glm::quat rotation = glm::angleAxis(angle, rotAxis); + + // Apply rotation to the joint + currentJoint->rotation = rotation * currentJoint->rotation; + } +} +---- + +The FABRIK algorithm: + +1. Works by alternating between forward and backward passes along the joint chain +2. In the backward pass, it positions joints working from the end effector toward the root +3. In the forward pass, it repositions joints from the root toward the end effector +4. This process quickly converges to a solution that satisfies the constraints + +===== IK Constraints + +In practice, IK systems need constraints to produce realistic results: + +[source,cpp] +---- +// Apply joint constraints to a node +void applyJointConstraints(Node* node, + const glm::vec3& minAngles, + const glm::vec3& maxAngles) { + // Convert quaternion to Euler angles + glm::vec3 eulerAngles = glm::degrees(glm::eulerAngles(node->rotation)); + + // Apply constraints + eulerAngles.x = glm::clamp(eulerAngles.x, minAngles.x, maxAngles.x); + eulerAngles.y = glm::clamp(eulerAngles.y, minAngles.y, maxAngles.y); + eulerAngles.z = glm::clamp(eulerAngles.z, minAngles.z, maxAngles.z); + + // Convert back to quaternion + glm::quat constrainedRotation = glm::quat(glm::radians(eulerAngles)); + + // Apply the constrained rotation + node->rotation = constrainedRotation; +} +---- + +===== Integrating IK with Animation + +Now that we've implemented several IK algorithms, let's see how they integrate with our animation system. Remember that our animation system updates node transformations based on keyframes, but sometimes we need to override or adjust these transformations based on runtime conditions. Here's how we can blend IK adjustments with our existing animation playback: + +[source,cpp] +---- +// Apply IK on top of an animation +void applyIKToAnimation(Model* model, uint32_t animationIndex, float deltaTime, + Node* endEffector, const glm::vec3& targetPosition, + float ikWeight = 1.0f) { + // First, update the animation normally + model->updateAnimation(animationIndex, deltaTime); + + // If IK weight is zero, we're done + if (ikWeight <= 0.0f) return; + + // Build the joint chain from end effector to root + std::vector chain; + Node* current = endEffector; + + // Add up to 3 joints to the chain (e.g., hand, elbow, shoulder) + while (current && chain.size() < 3) { + chain.push_back(current); + current = current->parent; + } + + // Reverse the chain to go from root to end effector + std::reverse(chain.begin(), chain.end()); + + // Store original rotations + std::vector originalRotations; + for (Node* node : chain) { + originalRotations.push_back(node->rotation); + } + + // Apply IK + solveTwoBoneIK(chain[0], chain[1], chain[2], targetPosition, + glm::vec3(0.0f, 0.0f, 1.0f)); + + // Blend between original and IK rotations based on weight + if (ikWeight < 1.0f) { + for (size_t i = 0; i < chain.size(); i++) { + chain[i]->rotation = glm::slerp(originalRotations[i], + chain[i]->rotation, + ikWeight); + } + } +} +---- + +===== Use Cases and Limitations + +IK is powerful but comes with considerations: + +* *Performance*: Iterative IK algorithms can be computationally expensive +* *Stability*: IK can produce jittery results without proper damping and constraints +* *Realism*: Without constraints, IK can produce physically impossible poses +* *Integration*: Blending IK with existing animations requires careful tuning + +Despite these challenges, IK is essential for: + +* *Environmental Adaptation*: Making characters interact with varying terrain and objects +* *Procedural Animation*: Generating animations that respond to dynamic conditions +* *Interactive Gameplay*: Allowing precise control over character limbs for gameplay mechanics + +==== Animation State Machines + +So far, we've explored how to play individual animations, blend between animations, and adjust animations with IK. But in a real game, characters often have dozens of animations that need to be triggered based on player input and game state. How do we organize and manage all these animations and their transitions? This is where animation state machines come in. + +For complex characters, a state machine can manage transitions between animations: + +[source,cpp] +---- +enum class AnimationState { + IDLE, + WALKING, + RUNNING, + JUMPING +}; + +class CharacterAnimator { +private: + Model* model; + AnimationState currentState = AnimationState::IDLE; + float blendTime = 0.3f; + float currentBlend = 0.0f; + + struct StateAnimation { + uint32_t animationIndex; + float speed; + bool loop; + }; + + std::unordered_map stateMap; + +public: + CharacterAnimator(Model* model) : model(model) { + // Map states to animations + stateMap[AnimationState::IDLE] = {0, 1.0f, true}; + stateMap[AnimationState::WALKING] = {1, 1.0f, true}; + stateMap[AnimationState::RUNNING] = {2, 1.0f, true}; + stateMap[AnimationState::JUMPING] = {3, 1.0f, false}; + } + + void setState(AnimationState newState) { + if (newState != currentState) { + // Start blending to new animation + currentBlend = 0.0f; + currentState = newState; + } + } + + void update(float deltaTime) { + // Handle blending if needed + if (currentBlend < blendTime) { + currentBlend += deltaTime; + float t = currentBlend / blendTime; + // Implement blending logic here + } else { + // Just update current animation + StateAnimation& anim = stateMap[currentState]; + model->updateAnimation(anim.animationIndex, deltaTime * anim.speed); + } + } +}; +---- + +==== Procedural Animations + +You can also create animations procedurally: + +[source,cpp] +---- +void applyProceduralAnimation(float time) { + // Find the head node + Node* headNode = nullptr; + for (auto node : model.linearNodes) { + if (node->name == "Head") { + headNode = node; + break; + } + } + + if (headNode) { + // Apply a simple bobbing motion + float bobAmount = sin(time * 2.0f) * 0.05f; + headNode->translation.y += bobAmount; + + // Apply a simple looking around motion + float lookAmount = sin(time * 0.5f) * 0.2f; + glm::quat lookRotation = glm::angleAxis(lookAmount, glm::vec3(0.0f, 1.0f, 0.0f)); + headNode->rotation = lookRotation * headNode->rotation; + } +} +---- + +=== Performance Considerations + +Animations can be computationally expensive, especially with complex models. Here are some optimization techniques: + +* *Level of Detail (LOD)*: Use simpler animations for distant objects +* *Animation Culling*: Don't update animations for objects outside the view frustum +* *Keyframe Reduction*: Reduce the number of keyframes in animations that don't need high precision +* *Parallel Processing*: Update animations in parallel using multiple threads + +=== Conclusion + +Our animation system provides a solid foundation for bringing 3D models to life. By leveraging the glTF format and our scene graph structure, we can efficiently load, play, and blend animations to create dynamic and engaging scenes. + +In the next chapter, we'll wrap up our exploration of the model loading system and discuss future enhancements. + +link:07_scene_rendering.adoc[Previous: Rendering the Scene] | link:09_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc b/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc new file mode 100644 index 00000000..d3fe3fd0 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc @@ -0,0 +1,35 @@ +::pp: {plus}{plus} + += Loading Models: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've completed our simple engine by integrating model loading capabilities with the architecture and camera systems developed in the previous chapters. Building upon our knowledge of glTF from the link:../../15_GLTF_KTX2_Migration.html[main tutorial], we've implemented: + +1. A hierarchical scene graph for organizing 3D objects +2. Support for glTF animations +3. A PBR material system that leverages glTF's material properties +4. Multi-object rendering with individual transformations + +This approach demonstrates how the concepts learned throughout this tutorial series can be structured into a more reusable and extensible engine architecture. By combining the engine architecture principles, camera transformation systems, and now model loading capabilities, we've created a foundation that you can build upon for your own projects. + +As you continue to develop your engine, consider exploring these advanced topics: + +1. A more sophisticated material system +2. Advanced lighting techniques +3. Post-processing effects +4. Physics integration +5. Audio systems + +The code for this chapter can be found in the `simple_engine/20_loading_models.cpp` file. + +link:../../attachments/simple_engine/20_loading_models.cpp[C{pp} code] + +link:08_animations.adoc[Previous: Updating Animations] | link:../Subsystems/01_introduction.adoc[Next: Subsystems] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Loading_Models/index.adoc b/en/Building_a_Simple_Engine/Loading_Models/index.adoc new file mode 100644 index 00000000..9bc778ff --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/index.adoc @@ -0,0 +1,30 @@ +::pp: {plus}{plus} + += Loading Models: Integrating a glTF loader with animation and PBR +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Chapter Overview + +Welcome to the third chapter of the "Building a Simple Engine" series! After exploring engine architecture and camera systems in the previous chapters, we'll now focus on creating a robust model loading system that can handle modern 3D assets using the glTF format. We'll implement a scene graph, animation system, and PBR rendering to complete our engine foundation. + +This chapter is divided into several sections to make it easier to follow: + +1. link:01_introduction.adoc[Introduction] - An overview of what we'll be building and prerequisites +2. link:02_project_setup.adoc[Setting Up the Project] - How to structure our engine project +3. link:03_model_system.adoc[Implementing the Model Loading System] - Creating the core data structures +4. link:04_loading_gltf.adoc[Loading a glTF Model] - Parsing and processing glTF files +5. link:05_pbr_rendering.adoc[Implementing PBR Rendering] - Setting up physically-based rendering +6. link:06_multiple_objects.adoc[Rendering Multiple Objects] - Managing multiple model instances +7. link:07_scene_rendering.adoc[Rendering the Scene] - Drawing the scene graph +8. link:08_animations.adoc[Updating Animations] - Animating models +9. link:09_conclusion.adoc[Conclusion] - Summary and future directions + +Each section builds upon the previous ones, so it's recommended to follow them in order. This chapter also builds upon the concepts introduced in the Engine Architecture and Camera Transformations chapters. By completing this chapter, you'll have a comprehensive foundation for a Vulkan-based 3D engine with a well-structured architecture, camera system, and model loading capabilities. + +link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc new file mode 100644 index 00000000..d9b4cb91 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc @@ -0,0 +1,40 @@ +::pp: {plus}{plus} + += Mobile Development: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction to Mobile Development + +In previous chapters, we've built a solid foundation for our simple engine, implementing core components like the rendering pipeline, camera systems, model loading, essential subsystems, and tooling. Now, we're ready to explore how to adapt our engine for mobile platforms, specifically Android and iOS. + +Mobile development presents unique challenges and opportunities for Vulkan applications. The constraints of mobile hardware—limited power, memory, and thermal capacity—require careful optimization and consideration of platform-specific features. At the same time, mobile platforms offer exciting possibilities for reaching a wider audience with your applications. + +=== What We'll Cover + +In this chapter, we'll explore: + +* *Platform Considerations for Android and iOS*: We'll discuss the specific requirements and constraints of developing Vulkan applications for Android and iOS, including platform-specific setup, lifecycle management, and input handling. + +* *Performance Optimizations for Mobile*: We'll explore techniques for optimizing your engine for mobile hardware, focusing on power-of-two textures, efficient texture formats, and other mobile-specific optimizations. + +* *Rendering Approaches*: We'll compare Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR), understanding their implications for mobile GPU architectures. + +* *Vulkan Extensions for Mobile*: We'll explore extensions like VK_KHR_dynamic_rendering_local_read, VK_KHR_dynamic_rendering, and VK_EXT_shader_tile_image that can significantly improve performance on mobile devices. + +=== Prerequisites + +Before diving into this chapter, you should be familiar with: + +* The basics of Vulkan and our engine architecture from previous chapters +* Modern C++ concepts, particularly those introduced in C++17 and C++20 +* Basic understanding of mobile development concepts + +Let's begin by exploring the platform considerations for Android and iOS. + +link:../Tooling/07_conclusion.adoc[Previous: Tooling Conclusion] | link:02_platform_considerations.adoc[Next: Platform Considerations for Android and iOS] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc new file mode 100644 index 00000000..57b21031 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc @@ -0,0 +1,199 @@ +::pp: {plus}{plus} + += Mobile Development: Platform Considerations +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Platform Considerations for Android and iOS + +Developing Vulkan applications for mobile platforms requires understanding the specific requirements and constraints of Android and iOS. In this section, we'll explore the key considerations for each platform and how to adapt your engine accordingly. + +=== Android Platform Considerations + +Android has supported Vulkan since version 7.0 (Nougat), but the level of support varies across devices. Here are the key considerations for Android development: + +==== Setting Up Vulkan on Android + +To use Vulkan on Android, you need to: + +* *Declare Vulkan Support*: In your AndroidManifest.xml, declare that your +application uses Vulkan: + +[source,xml] +---- + + + + + + + +---- + +* *Initialize Vulkan*: Use the Android Native Development Kit (NDK) to +initialize Vulkan. The process is similar to desktop Vulkan, but you'll need + to obtain the native window handle from Android: + +[source,cpp] +---- +// Get the native window handle from Android +ANativeWindow* native_window = ANativeWindow_fromSurface(env, surface); + +// Create the Vulkan surface +VkAndroidSurfaceCreateInfoKHR create_info{}; +create_info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; +create_info.window = native_window; + +VkSurfaceKHR vulkan_surface; +vkCreateAndroidSurfaceKHR(instance, &create_info, nullptr, &vulkan_surface); +---- + +==== Android Lifecycle Management + +Android applications have a complex lifecycle that you need to handle properly: + +1. *Activity Pausing and Resuming*: When your application is paused (e.g., when the user switches to another app), you should release Vulkan resources and recreate them when the application resumes. + +2. *Surface Changes*: The surface can change due to configuration changes (e.g., rotation). You need to handle these changes by recreating the swapchain. + +3. *Memory Pressure*: Android can reclaim memory from your application at any time. Design your engine to handle memory pressure gracefully. + +==== Android Input Handling + +Android input handling differs from desktop: + +1. *Touch Input*: Instead of mouse input, you'll need to handle touch events, including multi-touch gestures. + +2. *Sensors*: Android devices have various sensors (accelerometer, gyroscope, etc.) that you can use for input. + +==== Huawei Device Considerations + +Huawei devices represent a significant portion of the Android market and have some specific considerations: + +1. *HarmonyOS/EMUI*: Huawei's custom operating system is based on Android but has some differences. Test your application specifically on Huawei devices to ensure compatibility. + +2. *Huawei GPU Turbo*: This technology optimizes GPU performance and power efficiency. Your Vulkan application can benefit from this, but you should test to ensure compatibility. + +3. *AppGallery Distribution*: Since Huawei devices may not have Google Play Services, consider distributing your application through Huawei's AppGallery. + +4. *Kirin SoCs*: Huawei's Kirin processors use Mali GPUs which are tile-based renderers. Optimize your rendering pipeline accordingly using the techniques described in the TBR section. + +[source,cpp] +---- +// Example of checking for Huawei devices +bool is_huawei_device(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + return props.vendorID == 0x19E5; // Huawei vendor ID +} + +// You can then apply Huawei-specific optimizations if needed +void configure_for_device(vk::PhysicalDevice physical_device) { + if (is_huawei_device(physical_device)) { + // Apply Huawei-specific optimizations + // For example, you might want to use specific memory allocation strategies + // or enable certain features that work well on Huawei's implementation + } +} +---- + +=== iOS Platform Considerations + +iOS supports Vulkan through MoltenVK, a translation layer that maps Vulkan to Metal. Here are the key considerations for iOS development: + +==== Setting Up MoltenVK on iOS + +To use Vulkan on iOS, you need to: + +* *Include MoltenVK*: Add the MoltenVK framework to your Xcode project. + +* *Initialize MoltenVK*: Initialize MoltenVK before creating your Vulkan +instance: + +[source,cpp] +---- +// Initialize MoltenVK +MVKConfiguration config{}; +vkGetMoltenVKConfigurationMVK(nullptr, &config); +config.debugMode = true; // Enable debug mode during development +vkSetMoltenVKConfigurationMVK(nullptr, &config); + +// Create Vulkan instance as usual +// ... +---- + +* *Create a Metal-Compatible Surface*: Create a Vulkan surface from a +CAMetalLayer: + +[source,cpp] +---- +// Get the Metal layer from your UIView +CAMetalLayer* metal_layer = (CAMetalLayer*)layer; + +// Create the Vulkan surface +VkMetalSurfaceCreateInfoEXT create_info{}; +create_info.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; +create_info.pLayer = metal_layer; + +VkSurfaceKHR vulkan_surface; +vkCreateMetalSurfaceEXT(instance, &create_info, nullptr, &vulkan_surface); +---- + +==== iOS Lifecycle Management + +iOS applications also have a lifecycle that you need to handle: + +1. *Application State Changes*: Handle applicationWillResignActive, applicationDidBecomeActive, etc., by releasing and recreating Vulkan resources as needed. + +2. *Memory Warnings*: iOS can send memory warnings when the system is low on memory. Handle these by releasing non-essential resources. + +==== iOS Input Handling + +iOS input handling is similar to Android but with some differences: + +1. *Touch Input*: iOS has its own touch event system that you'll need to integrate with your engine. + +2. *Sensors*: iOS devices also have various sensors that you can use for input. + +=== Cross-Platform Considerations + +To maintain a single codebase for both Android and iOS (and potentially desktop), consider: + +* *Abstraction Layers*: Create platform-specific abstraction layers for +window creation, input handling, and other platform-specific functionality. + +* *Conditional Compilation*: Use preprocessor directives to handle +platform-specific code: + +[source,cpp] +---- +#ifdef __ANDROID__ + // Android-specific code +#elif defined(__APPLE__) + // iOS-specific code +#else + // Desktop-specific code +#endif +---- + +* *Feature Detection*: Use Vulkan's feature detection mechanisms to adapt to +the capabilities of the device, rather than making assumptions based on the platform. + +=== Best Practices for Mobile Platform Integration + +1. *Test on Real Devices*: Emulators and simulators may not accurately represent the performance and behavior of real devices. + +2. *Handle Different Screen Sizes and Aspect Ratios*: Mobile devices come in various sizes and aspect ratios. Design your UI and rendering to adapt accordingly. + +3. *Consider Battery Life*: Mobile users are sensitive to battery drain. Optimize your engine to minimize power consumption. + +4. *Respect Platform Guidelines*: Follow the design and user experience guidelines for each platform to ensure your application feels native. + +In the next section, we'll explore performance optimizations specifically tailored for mobile hardware, focusing on texture formats and memory usage. + +link:01_introduction.adoc[Previous: Introduction] | link:03_performance_optimizations.adoc[Next: Performance Optimizations for Mobile] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc new file mode 100644 index 00000000..53d02ac3 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -0,0 +1,359 @@ +::pp: {plus}{plus} + += Mobile Development: Performance Optimizations +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Performance Optimizations for Mobile + +Mobile devices have significantly different hardware constraints compared to desktop systems. In this section, we'll explore key performance optimizations that are essential for achieving good performance on mobile platforms. + +=== Texture Optimizations + +Textures are often the largest consumers of memory in a graphics application. Optimizing them is crucial for mobile performance. + +==== Power-of-Two Textures + +One of the most important optimizations for mobile is using power-of-two (POT) texture dimensions (e.g., 256x256, 512x1024, etc.). Here's why: + +1. *Hardware Acceleration*: Most mobile GPUs are optimized for POT textures and can process them more efficiently. + +2. *Mipmapping*: POT textures are required for proper mipmapping, which is essential for performance and visual quality. + +3. *Memory Alignment*: POT textures align better with memory pages, improving memory access patterns. + +To ensure your textures are POT, you can implement a texture loading system that automatically resizes non-POT textures: + +[source,cpp] +---- +vk::Extent2D make_power_of_two(uint32_t width, uint32_t height) { + // Find the next power of two for each dimension + uint32_t pot_width = 1; + while (pot_width < width) pot_width *= 2; + + uint32_t pot_height = 1; + while (pot_height < height) pot_height *= 2; + + return vk::Extent2D(pot_width, pot_height); +} + +void load_texture(const std::string& path) { + // Load the image data + int width, height, channels; + stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha); + + if (!pixels) { + throw std::runtime_error("Failed to load texture image: " + path); + } + + // Check if the dimensions are power of two + bool is_pot = (width & (width - 1)) == 0 && (height & (height - 1)) == 0; + + if (!is_pot) { + // Resize to power of two + vk::Extent2D pot_size = make_power_of_two(width, height); + + stbi_uc* resized_pixels = new stbi_uc[pot_size.width * pot_size.height * 4]; + stbir_resize_uint8(pixels, width, height, 0, + resized_pixels, pot_size.width, pot_size.height, 0, 4); + + stbi_image_free(pixels); + pixels = resized_pixels; + width = pot_size.width; + height = pot_size.height; + } + + // Create Vulkan image and upload pixels + // ... + + // Free the pixel data + stbi_image_free(pixels); +} +---- + +==== Efficient Texture Formats + +Choosing the right texture format is crucial for mobile performance: + +1. *Compressed Formats*: Use hardware-supported compressed formats whenever possible: + - *ASTC* (Adaptive Scalable Texture Compression): Widely supported on modern mobile GPUs, offers excellent quality-to-size ratio with flexible block sizes. + - *ETC2/EAC*: Required for OpenGL ES 3.0 and supported by most Android devices. + - *PVRTC*: Primarily supported on iOS devices with PowerVR GPUs. + - *BC* (Block Compression): More common on desktop but supported by some high-end mobile GPUs. + +2. *Format Selection Based on Content*: Choose formats based on the type of texture: + - Use ASTC 4x4 or 6x6 for normal maps and detailed textures. + - Use ASTC 8x8 or 12x12 for diffuse/albedo textures. + - Consider using R8 for single-channel textures like height maps. + +Here's how to check for and use compressed formats in Vulkan: + +[source,cpp] +---- +bool is_format_supported(vk::PhysicalDevice physical_device, vk::Format format, vk::ImageTiling tiling, + vk::FormatFeatureFlags features) { + vk::FormatProperties props = physical_device.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear) { + return (props.linearTilingFeatures & features) == features; + } else if (tiling == vk::ImageTiling::eOptimal) { + return (props.optimalTilingFeatures & features) == features; + } + + return false; +} + +vk::Format find_supported_format(vk::PhysicalDevice physical_device, + const std::vector& candidates, + vk::ImageTiling tiling, + vk::FormatFeatureFlags features) { + for (vk::Format format : candidates) { + if (is_format_supported(physical_device, format, tiling, features)) { + return format; + } + } + + throw std::runtime_error("Failed to find supported format"); +} + +vk::Format find_best_compressed_format(vk::PhysicalDevice physical_device) { + // Try ASTC formats first (best quality-to-size ratio) + std::vector astc_formats = { + vk::Format::eAstc8x8SrgbBlock, + vk::Format::eAstc6x6SrgbBlock, + vk::Format::eAstc4x4SrgbBlock + }; + + try { + return find_supported_format( + physical_device, + astc_formats, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eSampledImage + ); + } catch (const std::runtime_error&) { + // ASTC not supported, try ETC2 + } + + std::vector etc2_formats = { + vk::Format::eEtc2R8G8B8A8SrgbBlock, + vk::Format::eEtc2R8G8B8SrgbBlock + }; + + try { + return find_supported_format( + physical_device, + etc2_formats, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eSampledImage + ); + } catch (const std::runtime_error&) { + // ETC2 not supported, try BC + } + + std::vector bc_formats = { + vk::Format::eBc7SrgbBlock, + vk::Format::eBc3SrgbBlock, + vk::Format::eBc1SrgbBlock + }; + + try { + return find_supported_format( + physical_device, + bc_formats, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eSampledImage + ); + } catch (const std::runtime_error&) { + // Fall back to uncompressed + return vk::Format::eR8G8B8A8Srgb; + } +} +---- + +=== Memory Optimizations + +Memory is a precious resource on mobile devices. Here are some key optimizations: + +==== Minimize Memory Allocations + +1. *Pool Allocations*: Use memory pools to reduce the overhead of frequent allocations and deallocations. + +2. *Suballocate from Larger Blocks*: Instead of creating many small Vulkan memory allocations, allocate larger blocks and suballocate from them: + +[source,cpp] +---- +class VulkanMemoryPool { +public: + VulkanMemoryPool(vk::Device device, vk::PhysicalDevice physical_device, + vk::DeviceSize block_size, uint32_t memory_type_index) + : device(device), block_size(block_size), memory_type_index(memory_type_index) { + allocate_new_block(); + } + + ~VulkanMemoryPool() { + for (auto& block : memory_blocks) { + device.freeMemory(block.memory); + } + } + + struct Allocation { + vk::DeviceMemory memory; + vk::DeviceSize offset; + vk::DeviceSize size; + }; + + Allocation allocate(vk::DeviceSize size, vk::DeviceSize alignment) { + // Find a block with enough space + for (auto& block : memory_blocks) { + vk::DeviceSize aligned_offset = align(block.next_offset, alignment); + if (aligned_offset + size <= block_size) { + Allocation alloc; + alloc.memory = block.memory; + alloc.offset = aligned_offset; + alloc.size = size; + + block.next_offset = aligned_offset + size; + return alloc; + } + } + + // No block has enough space, allocate a new one + allocate_new_block(); + return allocate(size, alignment); // Try again with the new block + } + +private: + struct MemoryBlock { + vk::DeviceMemory memory; + vk::DeviceSize next_offset = 0; + }; + + void allocate_new_block() { + vk::MemoryAllocateInfo alloc_info; + alloc_info.setAllocationSize(block_size); + alloc_info.setMemoryTypeIndex(memory_type_index); + + MemoryBlock block; + block.memory = device.allocateMemory(alloc_info); + block.next_offset = 0; + + memory_blocks.push_back(block); + } + + vk::DeviceSize align(vk::DeviceSize offset, vk::DeviceSize alignment) { + return (offset + alignment - 1) & ~(alignment - 1); + } + + vk::Device device; + vk::DeviceSize block_size; + uint32_t memory_type_index; + std::vector memory_blocks; +}; +---- + +==== Reduce Bandwidth Usage + +1. *Minimize State Changes*: Group draw calls by material to reduce state changes. + +2. *Use Smaller Data Types*: Use 16-bit indices and half-precision floats where appropriate. + +3. *Optimize Vertex Formats*: Use packed vertex formats to reduce memory bandwidth: + +[source,cpp] +---- +// Traditional vertex format (48 bytes per vertex) +struct Vertex { + glm::vec3 position; // 12 bytes + glm::vec3 normal; // 12 bytes + glm::vec2 texCoord; // 8 bytes + glm::vec4 color; // 16 bytes +}; + +// Optimized vertex format (16 bytes per vertex) +struct OptimizedVertex { + // Position: 3 components, 16-bit float each + uint16_t position[3]; // 6 bytes + + // Normal: 2 components (can reconstruct Z), 8-bit signed normalized + int8_t normal[2]; // 2 bytes + + // TexCoord: 2 components, 16-bit float each + uint16_t texCoord[2]; // 4 bytes + + // Color: 4 components, 8-bit unsigned normalized + uint8_t color[4]; // 4 bytes +}; +---- + +=== Draw Call Optimizations + +Mobile GPUs are particularly sensitive to draw call overhead: + +1. *Instancing*: Use instancing to reduce draw calls for repeated objects. + +2. *Batching*: Combine multiple objects into a single mesh where possible. + +3. *Level of Detail (LOD)*: Implement LOD systems to reduce geometry complexity for distant objects. + +=== Vendor-Specific Optimizations + +Different mobile GPU vendors have specific architectures that may benefit from targeted optimizations. + +==== Huawei Kirin GPU Optimizations + +Huawei's Kirin SoCs typically use ARM Mali GPUs, but with custom configurations and optimizations: + +1. *GPU Turbo Technology*: Huawei's GPU Turbo technology can significantly improve performance and power efficiency. To take advantage of this: + - Maintain stable frame rates rather than pushing for maximum frames + - Avoid unnecessary GPU state changes + - Use efficient rendering techniques that work well with Mali GPUs + +2. *Memory Management*: Kirin SoCs often have unified memory architecture: + - Use `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` memory when possible + - Take advantage of the fast CPU-GPU memory transfers + +3. *Texture Compression*: Huawei devices support ASTC texture compression: + +[source,cpp] +---- +// Check for ASTC support on Huawei devices +bool supports_astc(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); + return features.textureCompressionASTC_LDR; +} + +// Prioritize ASTC for Huawei devices +vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + + // If it's a Huawei device and supports ASTC, prioritize it + if (props.vendorID == 0x19E5 && supports_astc(physical_device)) { + return vk::Format::eAstc8x8SrgbBlock; // Or another ASTC format based on your needs + } + + // Otherwise, fall back to the general format selection + return find_best_compressed_format(physical_device); +} +---- + +4. *Performance Monitoring*: Huawei provides performance monitoring tools that can help identify bottlenecks specific to their hardware. + +=== Best Practices for Mobile Performance + +1. *Profile on Target Devices*: Performance characteristics vary widely across mobile devices. Test on a range of hardware, including different Huawei models. + +2. *Monitor Temperature*: Mobile devices throttle performance when they get hot. Design your engine to adapt to thermal throttling. + +3. *Balance Quality and Performance*: Provide graphics settings that allow users to balance quality and performance based on their device capabilities. + +4. *Implement Adaptive Resolution*: Dynamically adjust rendering resolution based on performance metrics. + +In the next section, we'll explore different rendering approaches for mobile GPUs, focusing on the differences between Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR). + +link:02_platform_considerations.adoc[Previous: Platform Considerations] | link:04_rendering_approaches.adoc[Next: Rendering Approaches] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc new file mode 100644 index 00000000..cf9b8029 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -0,0 +1,304 @@ +::pp: {plus}{plus} + += Mobile Development: Rendering Approaches +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Rendering Approaches for Mobile GPUs + +Mobile GPUs typically use different rendering architectures compared to desktop GPUs. Understanding these differences is crucial for optimizing your Vulkan application for mobile platforms. In this section, we'll explore the two main rendering approaches: Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR). + +=== Tile-Based Rendering (TBR) + +Most modern mobile GPUs use a tile-based rendering architecture, also known as Tile-Based Deferred Rendering (TBDR) in some implementations. + +==== How TBR Works + +1. *Tiling Phase*: The screen is divided into small tiles (typically 16x16 or 32x32 pixels). + +2. *Binning Phase*: The GPU determines which primitives (triangles) affect each tile. + +3. *Rendering Phase*: For each tile: + a. Load the primitives affecting that tile into on-chip memory. + b. Render the primitives to the tile. + c. Write the completed tile back to main memory. + +==== Advantages of TBR + +1. *Reduced Memory Bandwidth*: Since rendering happens in on-chip memory, there's less traffic to main memory. + +2. *Power Efficiency*: Lower memory bandwidth means lower power consumption, which is crucial for battery-powered devices. + +3. *Hidden Surface Removal*: Many TBR GPUs perform early depth testing during the binning phase, reducing overdraw. + +==== Optimizing for TBR + +To get the best performance from TBR GPUs, consider these optimizations: + +* *Transient Attachments*: Use transient attachments for render targets that are only used within a render pass: + +[source,cpp] +---- +vk::AttachmentDescription depth_attachment{}; +depth_attachment.setFormat(depth_format); +depth_attachment.setSamples(vk::SampleCountFlagBits::e1); +depth_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +depth_attachment.setStoreOp(vk::AttachmentStoreOp::eDontCare); // Don't store the result +depth_attachment.setStencilLoadOp(vk::AttachmentLoadOp::eDontCare); +depth_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); +depth_attachment.setInitialLayout(vk::ImageLayout::eUndefined); +depth_attachment.setFinalLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal); + +// When creating the image, use the transient flag +vk::ImageCreateInfo image_info{}; +image_info.setImageType(vk::ImageType::e2D); +image_info.setExtent(vk::Extent3D(width, height, 1)); +image_info.setMipLevels(1); +image_info.setArrayLayers(1); +image_info.setFormat(depth_format); +image_info.setTiling(vk::ImageTiling::eOptimal); +image_info.setInitialLayout(vk::ImageLayout::eUndefined); +image_info.setUsage(vk::ImageUsageFlagBits::eDepthStencilAttachment); +image_info.setSamples(vk::SampleCountFlagBits::e1); +image_info.setFlags(vk::ImageCreateFlagBits::eTransient); // Transient flag +---- + +* *Render Pass Structure*: Design your render passes to take advantage of +tile-based rendering: + - Use subpasses to keep rendering operations within the tile memory. + - Use the right load/store operations to minimize memory traffic. + +[source,cpp] +---- +// Create a render pass with multiple subpasses +vk::SubpassDescription subpass1{}; +subpass1.setPipelineBindPoint(vk::PipelineBindPoint::eGraphics); +subpass1.setColorAttachments(color_attachment_refs); +subpass1.setDepthStencilAttachment(&depth_attachment_ref); + +vk::SubpassDescription subpass2{}; +subpass2.setPipelineBindPoint(vk::PipelineBindPoint::eGraphics); +subpass2.setInputAttachments(input_attachment_refs); // Use output from subpass1 as input +subpass2.setColorAttachments(final_color_attachment_refs); + +// Create a dependency to ensure proper ordering +vk::SubpassDependency dependency{}; +dependency.setSrcSubpass(0); +dependency.setDstSubpass(1); +dependency.setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); +dependency.setDstStageMask(vk::PipelineStageFlagBits::eFragmentShader); +dependency.setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); +dependency.setDstAccessMask(vk::AccessFlagBits::eInputAttachmentRead); + +// Create the render pass +vk::RenderPassCreateInfo render_pass_info{}; +render_pass_info.setAttachments(attachments); +render_pass_info.setSubpasses({subpass1, subpass2}); +render_pass_info.setDependencies(dependency); + +vk::RenderPass render_pass = device.createRenderPass(render_pass_info); +---- + +==== Best Practices for TBR + +* *Avoid Framebuffer Reads*: Reading from the framebuffer can force the tile to be written to main memory and then read back, which is expensive. + +* *Optimize for Tile Size*: Consider the tile size when designing your +rendering algorithm. For example, if you know the tile size is 16x16, you +might organize your data or algorithms to work efficiently with that size.Ok + +===== Memory Management + +To improve the efficiency of memory allocation in TBR architectures: + +* *Select Optimal Memory Types*: Choose the best matching memory type (with the appropriate VkMemoryPropertyFlags) when using vkAllocateMemory. + +* *Batch Allocations*: For each type of resource (e.g., index buffer, vertex buffer, and uniform buffer), allocate large chunks of memory with a specific size in one go when possible. + +* *Reuse Memory Resources*: Let multiple passes take turns using the allocated memory through time slicing. + +* *Use Cached Memory When Appropriate*: Consider using VK_MEMORY_PROPERTY_HOST_CACHED_BIT and manually flushing memory when it may be accessed by the CPU. This is often more efficient than VK_MEMORY_PROPERTY_HOST_COHERENT_BIT because the driver can refresh a large block of memory at once. + +* *Minimize Allocation Calls*: Avoid frequent calls to vkAllocateMemory. The number of memory allocations is limited by maxMemoryAllocationCount. + +===== Shader Optimizations + +Optimizing shaders for TBR architectures can significantly improve performance: + +* *Vectorized Memory Access*: Access memory in a vectorized manner to reduce access cycles and bandwidth. For example: + +[source,glsl] +---- +// Recommended: Vectorized access +struct TileStructSample { + vec4 data; +}; + +void main() { + uint idx = 0u; + TileStructSample ts[3]; + while (idx < 3u) { + ts[int(idx)].data = a; + idx++; + } +} + +// Not recommended: Non-vectorized access +struct TileStructSample { + float data1; + float data2; + float data3; + float data4; +}; + +void main() { + uint idx = 0u; + TileStructSample ts[3]; + while (idx < 3u) { + ts[int(idx)].data1 = a; + ts[int(idx)].data2 = b; + ts[int(idx)].data3 = c; + ts[int(idx)].data4 = d; + idx++; + } +} +---- + +* *Optimize Uniform Buffers*: Consider using push constants or macro constants instead of uniform buffers for small data. Avoid dynamic indexing when possible. + +* *Minimize Branching*: Reduce complex branch structures, branch nesting, and loop structures as they can harm parallelism. + +* *Use Half-Precision*: When appropriate, use half-precision floats to reduce bandwidth and power consumption. In SPIR-V, use relaxed-precision decoration on variables or results. + +===== Depth Testing Optimizations + +Proper depth testing is crucial for TBR performance: + +* *Enable Depth Testing and Writing*: This allows the GPU to cull hidden primitives and reduce overdraw. + +* *Avoid Operations That Disable Early-Z*: The following operations can prevent effective early depth testing: + - Using the discard instruction in fragment shaders + - Writing to gl_FragDepth explicitly + - Using storage images or storage buffers + - Using gl_SampleMask + - Enabling both depth bounds and depth write + - Enabling both blending and depth write + +* *Consistent Compare Operations*: When using compareOp, try to keep the values consistent for each draw in the render pass. + +* *Clear Attachments Properly*: Attachments should be cleared at the beginning of the render pass, or when no valid compareOp value is assigned to previous draw calls. + +=== Immediate Mode Rendering (IMR) + +Traditional desktop GPUs and some older mobile GPUs use an immediate mode rendering architecture. + +==== How IMR Works + +1. *Vertex Processing*: Process vertices and assemble primitives. + +2. *Rasterization*: Convert primitives to fragments. + +3. *Fragment Processing*: Process each fragment and write the result directly to the framebuffer in main memory. + +==== Advantages of IMR + +1. *Simplicity*: The rendering model is more straightforward and matches the traditional graphics pipeline. + +2. *Flexibility*: Some algorithms that require reading from the framebuffer are easier to implement. + +==== Optimizing for IMR + +If your target device uses IMR, consider these optimizations: + +1. *Front-to-Back Rendering*: Render opaque objects from front to back to minimize overdraw. + +2. *Early-Z*: Use depth testing to reject fragments early in the pipeline. + +3. *Occlusion Culling*: Implement occlusion culling to avoid rendering objects that won't be visible. + +=== Detecting Rendering Architecture + +Vulkan doesn't provide a direct way to determine if a GPU uses TBR or IMR. However, you can make educated guesses based on the device vendor and model: + +[source,cpp] +---- +bool is_likely_tbr_gpu(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + + // Most mobile GPUs from these vendors use TBR + if (props.vendorID == 0x5143) { // Qualcomm + return true; + } + if (props.vendorID == 0x1010) { // PowerVR (Imagination Technologies) + return true; + } + if (props.vendorID == 0x13B5) { // ARM Mali + return true; + } + if (props.vendorID == 0x19E5) { // Huawei + return true; + } + + // Apple GPUs are also TBR + if (props.vendorID == 0x106B) { // Apple + return true; + } + + // For other vendors, you might need to maintain a list of known TBR GPUs + // or just assume desktop GPUs are IMR and mobile GPUs are TBR + + return false; +} +---- + +=== Adapting to Both Architectures + +The best approach is to design your engine to work well on both TBR and IMR architectures: + +* *Detect the Architecture*: Use heuristics to detect the likely architecture. + +* *Conditional Optimizations*: Apply different optimizations based on the +detected architecture: + +[source,cpp] +---- +void configure_rendering_pipeline(vk::PhysicalDevice physical_device) { + bool is_tbr = is_likely_tbr_gpu(physical_device); + + if (is_tbr) { + // TBR optimizations + use_transient_attachments = true; + prioritize_subpass_dependencies = true; + avoid_framebuffer_reads = true; + } else { + // IMR optimizations + use_front_to_back_sorting = true; + prioritize_early_z = true; + implement_occlusion_culling = true; + } +} +---- + +* *Fallback Strategy*: If you can't determine the architecture, optimize for +TBR, as those optimizations generally don't harm IMR performance significantly. + +=== Best Practices for Both Architectures + +Regardless of the rendering architecture, these practices will help optimize performance: + +1. *Minimize State Changes*: Group draw calls by material to reduce state changes. + +2. *Batch Similar Objects*: Use instancing or batching to reduce draw call overhead. + +3. *Use Appropriate Synchronization*: Use the minimum synchronization required to ensure correct rendering. + +4. *Profile on Target Devices*: Always test your optimizations on actual target devices. + +In the next section, we'll explore Vulkan extensions that can help you optimize performance on mobile devices, particularly those that leverage the tile-based architecture. + +link:03_performance_optimizations.adoc[Previous: Performance Optimizations] | link:05_vulkan_extensions.adoc[Next: Vulkan Extensions for Mobile] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc new file mode 100644 index 00000000..a2339301 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -0,0 +1,326 @@ +::pp: {plus}{plus} + += Mobile Development: Vulkan Extensions +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Vulkan Extensions for Mobile + +Vulkan's extensibility is one of its greatest strengths, allowing hardware vendors to expose specialized features that can significantly improve performance. For mobile development, several extensions are particularly valuable as they can help optimize for the unique characteristics of mobile GPUs. In this section, we'll explore key Vulkan extensions that can enhance performance on mobile devices. + +=== VK_KHR_dynamic_rendering + +Dynamic rendering is a game-changing extension that simplifies the Vulkan rendering workflow by eliminating the need for explicit render pass and framebuffer objects. + +==== Overview + +The `VK_KHR_dynamic_rendering` extension (now part of Vulkan 1.3 core) allows you to begin and end rendering operations directly within a command buffer, without creating render pass and framebuffer objects. This is particularly beneficial for mobile development as it: + +1. *Simplifies Code*: Reduces the complexity of managing render passes and framebuffers. +2. *Enables More Flexible Rendering*: Makes it easier to implement techniques that don't fit well into the traditional render pass model. +3. *Reduces API Overhead*: Fewer objects to create and manage means less CPU overhead. + +==== Implementation + +Here's how to use dynamic rendering: + +[source,cpp] +---- +// Enable the extension when creating the device +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME +}; + +// Get function pointers (if not using Vulkan 1.3) +PFN_vkCmdBeginRenderingKHR vkCmdBeginRenderingKHR = + reinterpret_cast( + vkGetDeviceProcAddr(device, "vkCmdBeginRenderingKHR")); +PFN_vkCmdEndRenderingKHR vkCmdEndRenderingKHR = + reinterpret_cast( + vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR")); + +// Begin rendering +VkRenderingAttachmentInfoKHR color_attachment{}; +color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR; +color_attachment.imageView = color_image_view; +color_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +color_attachment.clearValue = clear_value; + +VkRenderingAttachmentInfoKHR depth_attachment{}; +depth_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR; +depth_attachment.imageView = depth_image_view; +depth_attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; +depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +depth_attachment.clearValue = depth_clear_value; + +VkRenderingInfoKHR rendering_info{}; +rendering_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR; +rendering_info.renderArea = render_area; +rendering_info.layerCount = 1; +rendering_info.colorAttachmentCount = 1; +rendering_info.pColorAttachments = &color_attachment; +rendering_info.pDepthAttachment = &depth_attachment; + +vkCmdBeginRenderingKHR(command_buffer, &rendering_info); + +// Record drawing commands +vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); +vkCmdDraw(command_buffer, vertex_count, 1, 0, 0); + +// End rendering +vkCmdEndRenderingKHR(command_buffer); +---- + +When using C++ bindings: + +[source,cpp] +---- +// Using vulkan.hpp +vk::RenderingAttachmentInfoKHR color_attachment; +color_attachment.setImageView(color_image_view); +color_attachment.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal); +color_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +color_attachment.setStoreOp(vk::AttachmentStoreOp::eStore); +color_attachment.setClearValue(clear_value); + +vk::RenderingAttachmentInfoKHR depth_attachment; +depth_attachment.setImageView(depth_image_view); +depth_attachment.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal); +depth_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +depth_attachment.setStoreOp(vk::AttachmentStoreOp::eDontCare); +depth_attachment.setClearValue(depth_clear_value); + +vk::RenderingInfoKHR rendering_info; +rendering_info.setRenderArea(render_area); +rendering_info.setLayerCount(1); +rendering_info.setColorAttachments(color_attachment); +rendering_info.setPDepthAttachment(&depth_attachment); + +command_buffer.beginRenderingKHR(rendering_info); + +// Record drawing commands +command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); +command_buffer.draw(vertex_count, 1, 0, 0); + +// End rendering +command_buffer.endRenderingKHR(); +---- + +=== VK_KHR_dynamic_rendering_local_read + +The `VK_KHR_dynamic_rendering_local_read` extension is particularly valuable for tile-based renderers as it allows shaders to read from attachments without forcing a tile to main memory and back. + +==== Overview + +This extension enhances dynamic rendering by allowing fragment shaders to read from color and depth/stencil attachments within the same rendering scope. On tile-based renderers, this means the reads can happen directly from tile memory, avoiding expensive round trips to main memory. + +Key benefits include: + +1. *Reduced Memory Bandwidth*: Reads happen from on-chip memory rather than main memory. +2. *Improved Performance*: Particularly for algorithms that need to read from previously written attachments. +3. *Power Efficiency*: Lower memory bandwidth means lower power consumption. + +==== Implementation + +To use this extension: + +[source,cpp] +---- +// Enable the extension when creating the device +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME +}; + +// Create a pipeline that reads from attachments +vk::PipelineRenderingCreateInfoKHR rendering_create_info; +rendering_create_info.setColorAttachmentCount(1); +rendering_create_info.setColorAttachmentFormats(color_format); +rendering_create_info.setDepthAttachmentFormat(depth_format); + +// Set up the attachment local read info +vk::AttachmentSampleCountInfoAMD sample_count_info; +sample_count_info.setColorAttachmentSamples(vk::SampleCountFlagBits::e1); +sample_count_info.setDepthStencilAttachmentSamples(vk::SampleCountFlagBits::e1); + +vk::RenderingAttachmentLocationInfoKHR location_info; +location_info.setColorAttachmentLocations(0); // Location 0 for the color attachment + +vk::RenderingInputAttachmentIndexInfoKHR input_index_info; +input_index_info.setColorInputAttachmentIndices(0); // Index 0 for the color attachment + +// Create the graphics pipeline +vk::GraphicsPipelineCreateInfo pipeline_info; +pipeline_info.setPNext(&rendering_create_info); +// ... set other pipeline creation parameters + +// In your fragment shader, you can now read from the attachment +// using subpassLoad() or texture sampling with the appropriate extension +// Fragment shader example (GLSL): +// #extension GL_EXT_shader_tile_image : require +// layout(location = 0) out vec4 outColor; +// layout(input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput inputColor; +// void main() { +// vec4 color = subpassLoad(inputColor); +// outColor = color * 2.0; // Double the brightness +// } +---- + +=== VK_EXT_shader_tile_image + +The `VK_EXT_shader_tile_image` extension provides direct access to tile memory in shaders, which can significantly improve performance on tile-based renderers. + +==== Overview + +This extension allows shaders to: + +1. *Access Tile Memory Directly*: Read and write to the current tile's memory without going through main memory. +2. *Perform Tile-Local Operations*: Execute operations that stay entirely within the tile memory. +3. *Optimize Bandwidth-Intensive Algorithms*: Particularly beneficial for post-processing effects. + +==== Implementation + +To use this extension: + +[source,cpp] +---- +// Enable the extension when creating the device +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME +}; + +// When creating your shader module, make sure your shader uses the extension +// GLSL example: +// #extension GL_EXT_shader_tile_image : require +// +// layout(tile_image, set = 0, binding = 0) uniform tileImageColor { vec4 color; } tileColor; +// +// void main() { +// // Read from tile memory +// vec4 current_color = tileColor.color; +// +// // Process the color +// vec4 new_color = process(current_color); +// +// // Write back to tile memory +// tileColor.color = new_color; +// } +---- + +=== Combining Extensions for Maximum Performance + +For the best mobile performance, consider using these extensions together: + +[source,cpp] +---- +// Enable all relevant extensions +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, + VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME +}; + +// Check which extensions are supported +auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); +std::vector supported_extensions; + +for (const auto& requested_ext : device_extensions) { + for (const auto& available_ext : available_extensions) { + if (strcmp(requested_ext, available_ext.extensionName) == 0) { + supported_extensions.push_back(requested_ext); + break; + } + } +} + +// Create device with supported extensions +vk::DeviceCreateInfo device_create_info; +device_create_info.setPEnabledExtensionNames(supported_extensions); +// ... set other device creation parameters +vk::Device device = physical_device.createDevice(device_create_info); + +// Now you can use the supported extensions in your rendering code +// ... +---- + +=== Vendor-Specific Extension Support + +Different mobile vendors may have varying levels of support for Vulkan extensions. Understanding these differences can help you optimize your application for specific hardware. + +==== Huawei Extension Support + +Huawei devices, particularly those with newer Kirin SoCs, have strong support for Vulkan extensions that can significantly improve performance: + +1. *Dynamic Rendering Support*: Huawei's implementation of `VK_KHR_dynamic_rendering` is highly optimized for their Mali-based GPUs. This can lead to significant performance improvements compared to traditional render passes. + +2. *Tile-Based Optimizations*: As Huawei devices use tile-based renderers, extensions like `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are particularly effective. These extensions can reduce memory bandwidth by up to 30% in some scenarios on Huawei hardware. + +3. *Checking for Huawei-Specific Support*: + +[source,cpp] +---- +bool check_huawei_extension_support(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + bool is_huawei = (props.vendorID == 0x19E5); + + if (!is_huawei) { + return false; + } + + // Check for extensions that work particularly well on Huawei devices + auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); + bool has_dynamic_rendering = false; + bool has_dynamic_rendering_local_read = false; + bool has_shader_tile_image = false; + + for (const auto& ext : available_extensions) { + std::string ext_name = ext.extensionName; + if (ext_name == VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) { + has_dynamic_rendering = true; + } else if (ext_name == VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME) { + has_dynamic_rendering_local_read = true; + } else if (ext_name == VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME) { + has_shader_tile_image = true; + } + } + + // Log the extension support + std::cout << "Huawei device detected with extension support:" << std::endl; + std::cout << " Dynamic Rendering: " << (has_dynamic_rendering ? "Yes" : "No") << std::endl; + std::cout << " Dynamic Rendering Local Read: " << (has_dynamic_rendering_local_read ? "Yes" : "No") << std::endl; + std::cout << " Shader Tile Image: " << (has_shader_tile_image ? "Yes" : "No") << std::endl; + + return has_dynamic_rendering || has_dynamic_rendering_local_read || has_shader_tile_image; +} +---- + +4. *Huawei-Specific Optimizations*: When developing for Huawei devices, consider these optimizations: + - Prioritize the use of dynamic rendering over traditional render passes + - Use tile-based extensions whenever available + - Test different configurations to find the optimal settings for specific Huawei models + +=== Best Practices for Using Extensions + +1. *Check for Support*: Always check if an extension is supported before using it. + +2. *Fallback Paths*: Implement fallback paths for when extensions aren't available. + +3. *Test on Real Devices*: Extensions may behave differently across vendors and devices, particularly between different Huawei models. + +4. *Stay Updated*: Keep track of new extensions that could benefit mobile performance, especially as Huawei continues to enhance their Vulkan support. + +In the next section, we'll conclude our exploration of mobile development with a summary of key takeaways and best practices. + +link:04_rendering_approaches.adoc[Previous: Rendering Approaches] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc new file mode 100644 index 00000000..955dd4c7 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -0,0 +1,276 @@ +::pp: {plus}{plus} + += Mobile Development: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've explored the key aspects of adapting your Vulkan engine for mobile platforms. Let's summarize what we've learned and discuss how to apply these techniques in your own projects. + +=== What We've Learned + +==== Platform Considerations for Android and iOS + +We started by examining the specific requirements and constraints of developing Vulkan applications for Android and iOS: + +* Setting up Vulkan on Android using the NDK +* Using MoltenVK to bring Vulkan to iOS +* Managing the complex lifecycle of mobile applications +* Handling platform-specific input methods +* Creating cross-platform abstractions to maintain a single codebase +* Addressing Huawei-specific considerations like HarmonyOS/EMUI and AppGallery distribution + +Understanding these platform-specific considerations is essential for creating a robust mobile application that behaves correctly across different devices and operating systems. + +==== Performance Optimizations for Mobile + +We then explored key performance optimizations for mobile hardware: + +* Using power-of-two textures for better hardware acceleration and memory alignment +* Selecting efficient texture formats like ASTC, ETC2, and PVRTC +* Minimizing memory allocations through pooling and suballocation +* Reducing bandwidth usage with optimized vertex formats and smaller data types +* Optimizing draw calls through instancing, batching, and LOD systems +* Leveraging vendor-specific optimizations, particularly for Huawei's Kirin SoCs with GPU Turbo technology + +These optimizations help address the limited resources available on mobile devices, ensuring your application runs smoothly while conserving battery life. + +==== Rendering Approaches: TBR vs IMR + +Next, we compared the two main rendering architectures found in mobile GPUs: + +* Tile-Based Rendering (TBR): How it works, its advantages, and specific optimizations +* Immediate Mode Rendering (IMR): How it works, its advantages, and specific optimizations +* Detecting the rendering architecture and adapting your engine accordingly +* Best practices that work well for both architectures +* Identifying TBR GPUs from major vendors including Qualcomm, PowerVR, ARM Mali, Apple, and Huawei + +Understanding these different rendering approaches allows you to optimize your engine for the specific hardware it's running on, maximizing performance across a wide range of devices. + +==== Vulkan Extensions for Mobile + +Finally, we explored Vulkan extensions that can significantly improve performance on mobile devices: + +* VK_KHR_dynamic_rendering: Simplifying the rendering workflow +* VK_KHR_dynamic_rendering_local_read: Enabling efficient reads from attachments in tile memory +* VK_EXT_shader_tile_image: Providing direct access to tile memory in shaders +* Combining these extensions for maximum performance +* Best practices for using extensions in a cross-platform engine +* Vendor-specific extension support, with particular focus on Huawei's optimized implementation of these extensions + +These extensions leverage the unique characteristics of mobile GPUs, particularly tile-based renderers, to achieve better performance and power efficiency. + +=== Putting It All Together + +Let's see how all these components can work together in a complete mobile-optimized Vulkan application: + +[source,cpp] +---- +class MobileOptimizedEngine { +public: + MobileOptimizedEngine() { + // Initialize platform-specific components + #ifdef __ANDROID__ + initialize_android(); + #elif defined(__APPLE__) + initialize_ios(); + #else + initialize_desktop(); + #endif + + // Initialize Vulkan with mobile optimizations + initialize_vulkan(); + } + + void run() { + // Main application loop + while (!should_close()) { + handle_platform_events(); + update(); + render(); + } + + cleanup(); + } + +private: + void initialize_vulkan() { + // Create instance + vk::InstanceCreateInfo instance_info; + // ... set instance parameters + instance = vk::createInstance(instance_info); + + // Select physical device + physical_device = select_physical_device(instance); + + // Detect if we're on a TBR GPU + is_tbr_gpu = is_likely_tbr_gpu(physical_device); + + // Check for extension support + auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); + std::vector supported_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + + // Add mobile-specific extensions if supported + if (check_extension_support(available_extensions, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME)) { + supported_extensions.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME); + use_dynamic_rendering = true; + } + + if (check_extension_support(available_extensions, VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME)) { + supported_extensions.push_back(VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME); + use_dynamic_rendering_local_read = true; + } + + if (check_extension_support(available_extensions, VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME)) { + supported_extensions.push_back(VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME); + use_shader_tile_image = true; + } + + // Create logical device with supported extensions + vk::DeviceCreateInfo device_info; + device_info.setPEnabledExtensionNames(supported_extensions); + // ... set other device parameters + device = physical_device.createDevice(device_info); + + // Initialize other Vulkan resources + // ... + } + + void render() { + // Begin frame + auto cmd_buffer = begin_frame(); + + if (use_dynamic_rendering) { + // Use dynamic rendering + vk::RenderingAttachmentInfoKHR color_attachment; + // ... set attachment parameters + + vk::RenderingInfoKHR rendering_info; + // ... set rendering parameters + + cmd_buffer.beginRenderingKHR(rendering_info); + + // Record drawing commands + // ... + + cmd_buffer.endRenderingKHR(); + } else { + // Use traditional render passes + // ... + } + + // End frame + end_frame(cmd_buffer); + } + + // Platform-specific initialization + void initialize_android() { + // Android-specific setup + // ... + } + + void initialize_ios() { + // iOS-specific setup with MoltenVK + // ... + } + + void initialize_desktop() { + // Desktop-specific setup + // ... + } + + // Helper functions + bool check_extension_support(const std::vector& available, const char* extension_name) { + for (const auto& ext : available) { + if (strcmp(extension_name, ext.extensionName) == 0) { + return true; + } + } + return false; + } + + bool is_likely_tbr_gpu(vk::PhysicalDevice device) { + vk::PhysicalDeviceProperties props = device.getProperties(); + + // Most mobile GPUs from these vendors use TBR + if (props.vendorID == 0x5143 || // Qualcomm + props.vendorID == 0x1010 || // PowerVR + props.vendorID == 0x13B5 || // ARM Mali + props.vendorID == 0x19E5 || // Huawei + props.vendorID == 0x106B) { // Apple + return true; + } + + return false; + } + + // Vulkan objects + vk::Instance instance; + vk::PhysicalDevice physical_device; + vk::Device device; + + // Flags + bool is_tbr_gpu = false; + bool use_dynamic_rendering = false; + bool use_dynamic_rendering_local_read = false; + bool use_shader_tile_image = false; +}; +---- + +=== Best Practices for Mobile Vulkan Development + +Based on what we've covered in this chapter, here are some best practices for mobile Vulkan development: + +1. *Design for Platform Differences*: Create abstractions that handle platform-specific differences while maintaining a single core codebase. + +2. *Optimize for Limited Resources*: Always consider the limited memory, bandwidth, and power available on mobile devices. + +3. *Adapt to the Rendering Architecture*: Optimize your rendering pipeline based on whether the device uses TBR or IMR. + +4. *Use Hardware-Accelerated Formats*: Choose texture formats that are natively supported by the hardware. + +5. *Leverage Vulkan Extensions*: Take advantage of mobile-specific extensions when available. + +6. *Test on Real Devices*: Emulators and simulators don't accurately represent real-world performance. + +7. *Monitor Performance Metrics*: Track frame times, memory usage, and power consumption to identify bottlenecks. + +8. *Provide Quality Options*: Allow users to adjust quality settings based on their device's capabilities. + +=== Future Directions + +Mobile graphics hardware continues to evolve rapidly. Here are some trends to watch: + +* *Unified Memory Architectures*: More mobile SoCs are adopting unified memory, which can change how you optimize memory access. +* *Ray Tracing on Mobile*: As ray tracing hardware becomes more common on mobile devices, new optimization techniques will emerge. +* *AI-Enhanced Rendering*: Mobile GPUs are increasingly incorporating AI acceleration, which can be leveraged for rendering tasks. +* *Cross-Platform Development*: Tools and frameworks for cross-platform development continue to improve, making it easier to target multiple platforms. +* *Huawei's GPU Innovations*: Huawei continues to advance their GPU technology with each generation of Kirin SoCs, introducing new features and optimizations that can be leveraged through Vulkan. + +=== Final Thoughts + +Developing for mobile platforms presents unique challenges, but also offers exciting opportunities to reach a wider audience. By understanding the specific characteristics of mobile hardware and optimizing your Vulkan engine accordingly, you can create high-performance applications that provide a great user experience while efficiently using the limited resources available on mobile devices. + +Remember that mobile optimization is an ongoing process. As new devices, architectures, and extensions emerge, continue to refine your engine to take advantage of these advancements. + +=== Code Examples + +The complete code for this chapter can be found in the following files: + +* `simple_engine/36_mobile_platform_integration.cpp`: Implementation of platform-specific integration for Android and iOS +* `simple_engine/37_mobile_optimizations.cpp`: Implementation of performance optimizations for mobile +* `simple_engine/38_tbr_optimizations.cpp`: Implementation of optimizations for tile-based renderers +* `simple_engine/39_mobile_extensions.cpp`: Implementation of mobile-specific Vulkan extensions + +link:../../attachments/simple_engine/36_mobile_platform_integration.cpp[Mobile Platform Integration C{pp} code] +link:../../attachments/simple_engine/37_mobile_optimizations.cpp[Mobile Optimizations C{pp} code] +link:../../attachments/simple_engine/38_tbr_optimizations.cpp[TBR Optimizations C{pp} code] +link:../../attachments/simple_engine/39_mobile_extensions.cpp[Mobile Extensions C{pp} code] + +link:05_vulkan_extensions.adoc[Previous: Vulkan Extensions for Mobile] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/index.adoc b/en/Building_a_Simple_Engine/Mobile_Development/index.adoc new file mode 100644 index 00000000..e8096b8b --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/index.adoc @@ -0,0 +1,21 @@ +::pp: {plus}{plus} + += Mobile Development +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +This chapter covers the essential aspects of adapting your Vulkan engine for mobile platforms, focusing on Android and iOS development, performance optimizations, rendering approaches, and mobile-specific Vulkan extensions. + +* link:01_introduction.adoc[Introduction] +* link:02_platform_considerations.adoc[Platform Considerations for Android and iOS] +* link:03_performance_optimizations.adoc[Performance Optimizations for Mobile] +* link:04_rendering_approaches.adoc[Rendering Approaches: TBR vs IMR] +* link:05_vulkan_extensions.adoc[Vulkan Extensions for Mobile] +* link:06_conclusion.adoc[Conclusion] + +link:../Tooling/07_conclusion.adoc[Previous: Tooling Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc new file mode 100644 index 00000000..5b6794a8 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc @@ -0,0 +1,56 @@ +::pp: {plus}{plus} + += Subsystems: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction to Engine Subsystems + +In previous chapters, we've built the foundation of our simple engine, implementing core components like the rendering pipeline, camera systems, and model loading. Now, we're ready to expand our engine's capabilities by adding two critical subsystems: Audio and Physics. + +These subsystems are essential for creating immersive and interactive experiences in modern games and simulations. While they may seem separate from the graphics pipeline we've been focusing on, modern engines can leverage Vulkan's computational power to enhance both audio processing and physics simulations. + +=== What We'll Cover + +In this chapter, we'll explore: + +* *Audio Subsystem*: We'll implement a basic audio system capable of playing sounds and music, and then explore how Vulkan compute shaders can be used for advanced audio processing techniques like Head-Related Transfer Function (HRTF) for 3D spatial audio. + +* *Physics Subsystem*: We'll create a simple physics system for collision detection and response, and then demonstrate how Vulkan compute shaders can accelerate physics calculations for large numbers of objects. + +Throughout this chapter, we'll continue our modern C++ approach from previous chapters. + +=== Why Vulkan for Audio and Physics? + +You might be wondering why we'd use a graphics API like Vulkan for audio processing and physics simulations. There are several compelling reasons: + +* *Computational Power*: Modern GPUs offer massive parallel processing capabilities that can be harnessed for non-graphical tasks through compute shaders. + +* *Unified Memory Model*: With Vulkan, we can share memory between graphics, audio, and physics processing, reducing data transfer overhead. + +* *Cross-Platform Consistency*: By using Vulkan for these subsystems, we maintain a consistent implementation across different platforms. + +* *Reduced CPU Load*: Offloading intensive calculations to the GPU frees up CPU resources for other game logic. + +Additionally, the intention here is to offer a perspective of using Vulkan +for more than just Graphics in your application. Our goal with this tutorial + isn't to provide you a production quality game engine. It's to provide you + with the tools necessary to tackle any Vulkan application development and to + think critically about how your applications can benefit from the GPU. + +=== Prerequisites + +Before diving into this chapter, you should be familiar with: + +* The basics of Vulkan and our engine architecture from previous chapters +* Compute shaders in Vulkan (covered in the main tutorial) +* Basic understanding of audio and physics concepts in game development + +Let's begin by exploring how to implement a basic audio subsystem and then enhance it with Vulkan's computational capabilities. + +link:../Loading_Models/09_conclusion.adoc[Previous: Loading Models Conclusion] | link:02_audio_basics.adoc[Next: Audio Basics] diff --git a/en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc b/en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc new file mode 100644 index 00000000..afc05d08 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc @@ -0,0 +1,243 @@ +::pp: {plus}{plus} + += Subsystems: Audio Basics +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Audio System Fundamentals + +Before we dive into how Vulkan can enhance audio processing, let's establish a foundation by implementing a basic audio system for our engine. This will give us a framework that we can later extend with Vulkan compute capabilities. + +=== Audio System Architecture + +A typical game audio system consists of several key components: + +* *Audio Engine*: The core component that manages audio playback, mixing, and effects processing. +* *Sound Resources*: Audio files loaded into memory and prepared for playback. +* *Audio Channels*: Logical paths for audio to flow through, often grouped by type (e.g., music, sound effects, dialogue). +* *Spatial Audio*: System for positioning sounds in 3D space relative to the listener. +* *Effects Processing*: Application of effects like reverb, echo, or equalization to audio streams. + +Let's implement a simple audio system that covers these basics, using a modern C++ approach consistent with our engine's design. + +=== Basic Audio System Implementation + +We'll start by defining the core classes for our audio system: + +[source,cpp] +---- +// Audio.h +#pragma once + +#include +#include +#include +#include +#include + +namespace Engine { +namespace Audio { + +class AudioClip { +public: + AudioClip(const std::string& filename); + ~AudioClip(); + + // Get raw audio data + const float* GetData() const { return m_Data.data(); } + size_t GetSampleCount() const { return m_Data.size(); } + int GetChannelCount() const { return m_ChannelCount; } + int GetSampleRate() const { return m_SampleRate; } + +private: + std::vector m_Data; + int m_ChannelCount; + int m_SampleRate; +}; + +class AudioSource { +public: + AudioSource(); + ~AudioSource(); + + void SetClip(std::shared_ptr clip) { m_Clip = clip; } + void SetPosition(const glm::vec3& position) { m_Position = position; } + void SetVolume(float volume) { m_Volume = volume; } + void SetLooping(bool looping) { m_Looping = looping; } + + void Play(); + void Stop(); + void Pause(); + + bool IsPlaying() const { return m_IsPlaying; } + + const glm::vec3& GetPosition() const { return m_Position; } + float GetVolume() const { return m_Volume; } + +private: + std::shared_ptr m_Clip; + glm::vec3 m_Position = glm::vec3(0.0f); + float m_Volume = 1.0f; + bool m_Looping = false; + bool m_IsPlaying = false; + + // Implementation-specific playback state + size_t m_CurrentSample = 0; +}; + +class AudioListener { +public: + void SetPosition(const glm::vec3& position) { m_Position = position; } + void SetOrientation(const glm::vec3& forward, const glm::vec3& up) { + m_Forward = forward; + m_Up = up; + } + + const glm::vec3& GetPosition() const { return m_Position; } + const glm::vec3& GetForward() const { return m_Forward; } + const glm::vec3& GetUp() const { return m_Up; } + +private: + glm::vec3 m_Position = glm::vec3(0.0f); + glm::vec3 m_Forward = glm::vec3(0.0f, 0.0f, -1.0f); + glm::vec3 m_Up = glm::vec3(0.0f, 1.0f, 0.0f); +}; + +class AudioSystem { +public: + AudioSystem(); + ~AudioSystem(); + + void Initialize(); + void Shutdown(); + + // Update audio system (call once per frame) + void Update(float deltaTime); + + // Resource management + std::shared_ptr LoadClip(const std::string& name, const std::string& filename); + std::shared_ptr GetClip(const std::string& name); + + // Source management + std::shared_ptr CreateSource(); + void DestroySource(std::shared_ptr source); + + // Listener (typically attached to camera) + AudioListener& GetListener() { return m_Listener; } + +private: + std::unordered_map> m_Clips; + std::vector> m_Sources; + AudioListener m_Listener; + + // Implementation-specific audio backend state + void* m_AudioBackend = nullptr; +}; + +} // namespace Audio +} // namespace Engine +---- + +This basic structure provides a foundation for loading and playing audio files with spatial positioning. In a real implementation, you would integrate with an audio library like OpenAL, FMOD, or Wwise to handle the low-level audio playback. + +=== Integrating with the Engine + +To integrate our audio system with the rest of our engine, we'll add it to our engine's main class: + +[source,cpp] +---- +// Engine.h +#include "Audio.h" + +namespace Engine { + +class Engine { +public: + // ... existing engine code ... + + Audio::AudioSystem& GetAudioSystem() { return m_AudioSystem; } + +private: + // ... existing engine members ... + + Audio::AudioSystem m_AudioSystem; +}; + +} // namespace Engine +---- + +And we'll initialize it during engine startup: + +[source,cpp] +---- +// Engine.cpp +void Engine::Initialize() { + // ... existing initialization code ... + + m_AudioSystem.Initialize(); +} + +void Engine::Shutdown() { + m_AudioSystem.Shutdown(); + + // ... existing shutdown code ... +} +---- + +=== Basic Usage Example + +Here's how you might use this audio system in a game: + +[source,cpp] +---- +// Game code +void Game::LoadResources() { + // Load audio clips + auto explosionSound = m_Engine.GetAudioSystem().LoadClip("explosion", "sounds/explosion.wav"); + auto backgroundMusic = m_Engine.GetAudioSystem().LoadClip("music", "sounds/background.ogg"); + + // Create and configure audio sources + m_MusicSource = m_Engine.GetAudioSystem().CreateSource(); + m_MusicSource->SetClip(backgroundMusic); + m_MusicSource->SetLooping(true); + m_MusicSource->SetVolume(0.5f); + m_MusicSource->Play(); +} + +void Game::OnExplosion(const glm::vec3& position) { + // Create a temporary source for the explosion sound + auto source = m_Engine.GetAudioSystem().CreateSource(); + source->SetClip(m_Engine.GetAudioSystem().GetClip("explosion")); + source->SetPosition(position); + source->Play(); + + // In a real implementation, you'd need to manage the lifetime of this source +} + +void Game::Update(float deltaTime) { + // Update listener position and orientation based on camera + auto& listener = m_Engine.GetAudioSystem().GetListener(); + listener.SetPosition(m_Camera.GetPosition()); + listener.SetOrientation(m_Camera.GetForward(), m_Camera.GetUp()); + + // Update audio system + m_Engine.GetAudioSystem().Update(deltaTime); +} +---- + +=== Limitations of Basic Audio Systems + +While this basic audio system provides the essential functionality for playing sounds in a game, it has several limitations: + +1. *Limited Spatial Audio*: Basic distance-based attenuation doesn't accurately model how sound propagates in 3D space. +2. *CPU-Intensive Processing*: Effects and 3D audio calculations can consume significant CPU resources. +3. *Limited Scalability*: Processing hundreds or thousands of sound sources can become a performance bottleneck. + +In the next section, we'll explore how Vulkan compute shaders can address these limitations by offloading audio processing to the GPU, particularly for implementing more realistic spatial audio through Head-Related Transfer Functions (HRTF). + +link:01_introduction.adoc[Previous: Introduction] | link:03_vulkan_audio.adoc[Next: Vulkan for Audio Processing] diff --git a/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc new file mode 100644 index 00000000..a8e828ad --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc @@ -0,0 +1,442 @@ +::pp: {plus}{plus} + += Subsystems: Vulkan for Audio Processing +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Enhancing Audio with Vulkan + +In the previous section, we implemented a basic audio system for our engine. Now, we'll explore how Vulkan's compute capabilities can enhance audio processing, particularly for implementing realistic 3D spatial audio using Head-Related Transfer Functions (HRTF). + +=== Understanding HRTF + +Head-Related Transfer Functions (HRTF) are a set of acoustic filters that model how sound is altered by the head, outer ear, and torso before reaching the eardrums. These filters vary based on the direction of the sound source relative to the listener. + +HRTF processing allows us to create convincing 3D audio by applying the appropriate filters to sound sources based on their position. This creates a more immersive experience than simple stereo panning and distance attenuation. + +The challenge with HRTF processing is that it's computationally expensive: + +1. Each sound source requires a unique set of filters based on its position +2. These filters must be applied to the audio stream in real-time +3. The process involves complex convolutions (multiplying audio samples with filter coefficients) + +This is where Vulkan compute shaders can help by offloading these calculations to the GPU. + +=== Why Use Vulkan for Audio Processing? + +Traditional audio processing is done on the CPU, but there are several advantages to using Vulkan compute shaders for certain audio tasks: + +1. *Parallelism*: Audio processing, especially HRTF convolution, can be highly parallelized, making it well-suited for GPU computation. +2. *Reduced CPU Load*: Offloading audio processing to the GPU frees up CPU resources for game logic, AI, and other tasks. +3. *Scalability*: GPU-based processing can more easily scale to handle hundreds or thousands of simultaneous sound sources. +4. *Unified Memory*: With Vulkan, we can share memory between graphics and audio processing, reducing data transfer overhead. + +=== Implementing HRTF Processing with Vulkan + +Let's extend our audio system to include HRTF processing using Vulkan compute shaders. + +First, we'll add HRTF-related structures to our audio system: + +[source,cpp] +---- +// Audio.h (additions) +#include +#include + +namespace Engine { +namespace Audio { + +// HRTF data for a specific direction +struct HRTFData { + std::array leftEarImpulseResponse; + std::array rightEarImpulseResponse; +}; + +// HRTF database containing filters for different directions +class HRTFDatabase { +public: + HRTFDatabase(const std::string& filename); + + // Get HRTF data for a specific direction + const HRTFData& GetHRTFData(float azimuth, float elevation) const; + +private: + // In a real implementation, this would be a more sophisticated data structure + std::vector m_Data; + // Mapping from direction to data index + // ... +}; + +// Extended AudioSystem with Vulkan-based HRTF processing +class AudioSystem { +public: + // ... existing methods ... + + // Enable/disable HRTF processing + void SetHRTFEnabled(bool enabled) { m_HRTFEnabled = enabled; } + bool IsHRTFEnabled() const { return m_HRTFEnabled; } + + // Set the HRTF database to use + void SetHRTFDatabase(std::shared_ptr database) { m_HRTFDatabase = database; } + +private: + // ... existing members ... + + // HRTF processing + bool m_HRTFEnabled = false; + std::shared_ptr m_HRTFDatabase; + + // Vulkan resources for HRTF processing + struct VulkanResources { + vk::raii::ShaderModule computeShaderModule = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline computePipeline = nullptr; + vk::raii::DescriptorPool descriptorPool = nullptr; + + // Buffers for audio data + vk::raii::Buffer inputBuffer = nullptr; + vk::raii::DeviceMemory inputBufferMemory = nullptr; + vk::raii::Buffer outputBuffer = nullptr; + vk::raii::DeviceMemory outputBufferMemory = nullptr; + vk::raii::Buffer hrtfBuffer = nullptr; + vk::raii::DeviceMemory hrtfBufferMemory = nullptr; + + // Descriptor sets + std::vector descriptorSets; + + // Command buffer for compute operations + vk::raii::CommandPool commandPool = nullptr; + vk::raii::CommandBuffer commandBuffer = nullptr; + }; + + VulkanResources m_VulkanResources; + + // Initialize Vulkan resources for HRTF processing + void InitializeVulkanResources(); + void CleanupVulkanResources(); + + // Process audio with HRTF using Vulkan + void ProcessAudioWithVulkan(float* inputBuffer, float* outputBuffer, size_t frameCount); +}; + +} // namespace Audio +} // namespace Engine +---- + +Now, let's implement the Vulkan-based HRTF processing: + +[source,cpp] +---- +// Audio.cpp (implementation) + +void AudioSystem::InitializeVulkanResources() { + // Get Vulkan device from the engine + auto& device = m_Engine.GetVulkanDevice(); + + // Create compute shader module + auto shaderCode = LoadShaderFile("shaders/hrtf_processing.comp.spv"); + vk::ShaderModuleCreateInfo shaderModuleCreateInfo({}, shaderCode.size() * sizeof(uint32_t), + reinterpret_cast(shaderCode.data())); + m_VulkanResources.computeShaderModule = vk::raii::ShaderModule(device, shaderModuleCreateInfo); + + // Create descriptor set layout + std::array bindings = { + // Input audio buffer + vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Output audio buffer + vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // HRTF data buffer + vk::DescriptorSetLayoutBinding(2, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute) + }; + + vk::DescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo({}, bindings); + m_VulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(device, descriptorSetLayoutCreateInfo); + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo({}, *m_VulkanResources.descriptorSetLayout); + m_VulkanResources.pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutCreateInfo); + + // Create compute pipeline + vk::PipelineShaderStageCreateInfo shaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.computeShaderModule, "main"); + vk::ComputePipelineCreateInfo computePipelineCreateInfo({}, shaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.computePipeline = vk::raii::Pipeline(device, nullptr, computePipelineCreateInfo); + + // Create descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 3) + }; + vk::DescriptorPoolCreateInfo descriptorPoolCreateInfo({}, 1, poolSizes); + m_VulkanResources.descriptorPool = vk::raii::DescriptorPool(device, descriptorPoolCreateInfo); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo descriptorSetAllocateInfo(*m_VulkanResources.descriptorPool, + 1, &*m_VulkanResources.descriptorSetLayout); + m_VulkanResources.descriptorSets = vk::raii::DescriptorSets(device, descriptorSetAllocateInfo); + + // Create buffers for audio data + // In a real implementation, you would size these appropriately and handle multiple frames + CreateBuffer(device, sizeof(float) * 1024, vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.inputBuffer, m_VulkanResources.inputBufferMemory); + CreateBuffer(device, sizeof(float) * 2048, vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.outputBuffer, m_VulkanResources.outputBufferMemory); + CreateBuffer(device, sizeof(float) * 512, vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.hrtfBuffer, m_VulkanResources.hrtfBufferMemory); + + // Update descriptor sets + std::array bufferInfos = { + vk::DescriptorBufferInfo(*m_VulkanResources.inputBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.outputBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.hrtfBuffer, 0, VK_WHOLE_SIZE) + }; + + std::array descriptorWrites = { + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 0, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[0]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 1, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[1]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 2, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[2]) + }; + + device.updateDescriptorSets(descriptorWrites, {}); + + // Create command pool and command buffer + vk::CommandPoolCreateInfo commandPoolCreateInfo({}, m_Engine.GetVulkanQueueFamilyIndex()); + m_VulkanResources.commandPool = vk::raii::CommandPool(device, commandPoolCreateInfo); + + vk::CommandBufferAllocateInfo commandBufferAllocateInfo(*m_VulkanResources.commandPool, + vk::CommandBufferLevel::ePrimary, 1); + auto commandBuffers = vk::raii::CommandBuffers(device, commandBufferAllocateInfo); + m_VulkanResources.commandBuffer = std::move(commandBuffers[0]); +} + +void AudioSystem::ProcessAudioWithVulkan(float* inputBuffer, float* outputBuffer, size_t frameCount) { + if (!m_HRTFEnabled || !m_HRTFDatabase) { + // If HRTF is disabled, just copy input to output (or do simple stereo panning) + memcpy(outputBuffer, inputBuffer, frameCount * sizeof(float)); + return; + } + + auto& device = m_Engine.GetVulkanDevice(); + auto& queue = m_Engine.GetVulkanComputeQueue(); + + // Copy input audio data to the input buffer + void* data; + vkMapMemory(device, *m_VulkanResources.inputBufferMemory, 0, frameCount * sizeof(float), 0, &data); + memcpy(data, inputBuffer, frameCount * sizeof(float)); + vkUnmapMemory(device, *m_VulkanResources.inputBufferMemory); + + // Update HRTF data based on source positions + // In a real implementation, you would update this for each sound source + // For simplicity, we're just using a single HRTF filter here + const auto& hrtfData = m_HRTFDatabase->GetHRTFData(0.0f, 0.0f); + vkMapMemory(device, *m_VulkanResources.hrtfBufferMemory, 0, sizeof(HRTFData), 0, &data); + memcpy(data, &hrtfData, sizeof(HRTFData)); + vkUnmapMemory(device, *m_VulkanResources.hrtfBufferMemory); + + // Record command buffer + vk::CommandBufferBeginInfo beginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); + m_VulkanResources.commandBuffer.begin(beginInfo); + + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *m_VulkanResources.computePipeline); + m_VulkanResources.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.pipelineLayout, 0, + *m_VulkanResources.descriptorSets[0], {}); + + // Dispatch compute shader + // The workgroup size should match what's defined in the shader + m_VulkanResources.commandBuffer.dispatch(frameCount / 64 + 1, 1, 1); + + m_VulkanResources.commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo({}, {}, *m_VulkanResources.commandBuffer); + queue.submit(submitInfo, nullptr); + queue.waitIdle(); + + // Copy output audio data from the output buffer + vkMapMemory(device, *m_VulkanResources.outputBufferMemory, 0, frameCount * 2 * sizeof(float), 0, &data); + memcpy(outputBuffer, data, frameCount * 2 * sizeof(float)); + vkUnmapMemory(device, *m_VulkanResources.outputBufferMemory); +} + +void AudioSystem::Update(float deltaTime) { + // Process all active audio sources + for (auto& source : m_Sources) { + if (source->IsPlaying()) { + // Get audio data from the source + auto clip = source->GetClip(); + if (!clip) continue; + + // Calculate spatial position relative to listener + glm::vec3 relativePosition = source->GetPosition() - m_Listener.GetPosition(); + + // Rotate relative position based on listener orientation + glm::mat3 listenerOrientation( + glm::cross(m_Listener.GetForward(), m_Listener.GetUp()), + m_Listener.GetUp(), + -m_Listener.GetForward() + ); + relativePosition = listenerOrientation * relativePosition; + + // Calculate azimuth and elevation + float distance = glm::length(relativePosition); + float azimuth = atan2(relativePosition.x, relativePosition.z); + float elevation = atan2(relativePosition.y, sqrt(relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z)); + + // Get audio data from the clip + const float* audioData = clip->GetData() + source->GetCurrentSample(); + size_t remainingSamples = clip->GetSampleCount() - source->GetCurrentSample(); + size_t framesToProcess = std::min(remainingSamples, size_t(1024)); + + // Process audio with HRTF using Vulkan + float processedAudio[2048]; // Stereo output (2 channels) + ProcessAudioWithVulkan(const_cast(audioData), processedAudio, framesToProcess); + + // Send processed audio to the audio backend + // ... + + // Update source state + source->IncrementSample(framesToProcess); + } + } +} +---- + +=== HRTF Compute Shader + +Here's the compute shader that performs the HRTF convolution: + +[source,glsl] +---- +// hrtf_processing.comp +#version 450 + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +// Input mono audio buffer +layout(std430, binding = 0) buffer InputBuffer { + float samples[]; +} inputBuffer; + +// Output stereo audio buffer +layout(std430, binding = 1) buffer OutputBuffer { + float leftSamples[]; + float rightSamples[]; +} outputBuffer; + +// HRTF data +layout(std430, binding = 2) buffer HRTFBuffer { + float leftImpulseResponse[256]; + float rightImpulseResponse[256]; +} hrtfBuffer; + +void main() { + uint gID = gl_GlobalInvocationID.x; + + // Check if this invocation is within the audio buffer + if (gID >= inputBuffer.samples.length()) { + return; + } + + // Perform convolution with HRTF impulse responses + float leftSample = 0.0; + float rightSample = 0.0; + + for (int i = 0; i < 256; i++) { + int sampleIndex = int(gID) - i; + if (sampleIndex >= 0 && sampleIndex < inputBuffer.samples.length()) { + leftSample += inputBuffer.samples[sampleIndex] * hrtfBuffer.leftImpulseResponse[i]; + rightSample += inputBuffer.samples[sampleIndex] * hrtfBuffer.rightImpulseResponse[i]; + } + } + + // Write to output buffer + outputBuffer.leftSamples[gID] = leftSample; + outputBuffer.rightSamples[gID] = rightSample; +} +---- + +=== Performance Considerations + +When implementing HRTF processing with Vulkan, consider these performance optimizations: + +1. *Batch Processing*: Process multiple audio frames in a single dispatch to amortize the overhead of command submission. +2. *Memory Transfers*: Minimize transfers between CPU and GPU memory by processing larger chunks of audio at once. +3. *Multiple Sources*: Process multiple sound sources in a single shader invocation to maximize GPU utilization. +4. *Dynamic HRTF Selection*: Only update HRTF filters when sound source positions change significantly. +5. *Workgroup Size*: Tune the workgroup size based on your target hardware for optimal performance. + +=== Integration with the Audio System + +To integrate the Vulkan-based HRTF processing into our audio system, we need to modify the `AudioSystem::Initialize` method: + +[source,cpp] +---- +void AudioSystem::Initialize() { + // Initialize audio backend + // ... + + // Initialize Vulkan resources for HRTF processing + if (m_Engine.IsVulkanInitialized()) { + InitializeVulkanResources(); + } + + // Load default HRTF database + m_HRTFDatabase = std::make_shared("data/hrtf/default.hrtf"); + m_HRTFEnabled = true; +} + +void AudioSystem::Shutdown() { + // Cleanup Vulkan resources + if (m_Engine.IsVulkanInitialized()) { + CleanupVulkanResources(); + } + + // Shutdown audio backend + // ... +} +---- + +=== Advantages of Vulkan-Based HRTF + +By implementing HRTF processing with Vulkan compute shaders, we gain several advantages: + +1. *Scalability*: The GPU can process hundreds or thousands of sound sources in parallel. +2. *Quality*: We can use higher-order HRTF filters without significant performance impact. +3. *CPU Offloading*: Audio processing no longer competes with game logic for CPU resources. +4. *Advanced Effects*: The GPU's computational power enables more complex audio effects like room acoustics simulation. + +=== Limitations and Considerations + +While Vulkan-based audio processing offers many advantages, there are some limitations to consider: + +1. *Latency*: GPU processing introduces additional latency, which may be problematic for real-time audio. +2. *Complexity*: Implementing and debugging GPU-based audio processing is more complex than CPU-based solutions. +3. *Platform Support*: Not all platforms support Vulkan, so you may need fallback CPU implementations. +4. *Power Consumption*: GPU processing may increase power consumption, which is a consideration for mobile devices. + +=== Real-World Applications + +Several modern game engines and audio middleware solutions are beginning to leverage GPU acceleration for audio processing: + +1. *Steam Audio*: Valve's audio SDK supports GPU acceleration for its spatial audio processing. +2. *Wwise*: Audiokinetic's Wwise can offload certain DSP effects to the GPU. +3. *Custom Solutions*: AAA game studios often implement custom GPU-accelerated audio processing for their titles. + +By implementing Vulkan-based HRTF processing in our engine, we're following industry best practices for high-performance audio in modern games. + +In the next section, we'll shift our focus to the physics subsystem and explore how Vulkan compute shaders can accelerate physics simulations. + +link:02_audio_basics.adoc[Previous: Audio Basics] | link:04_physics_basics.adoc[Next: Physics Basics] diff --git a/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc b/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc new file mode 100644 index 00000000..aa9bbf9c --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc @@ -0,0 +1,564 @@ +::pp: {plus}{plus} + += Subsystems: Physics Basics +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Physics System Fundamentals + +Before we explore how Vulkan can accelerate physics simulations, let's establish a foundation by implementing a basic physics system for our engine. This will give us a framework that we can later enhance with Vulkan compute capabilities. + +=== Physics System Architecture + +A typical game physics system consists of several key components: + +* *Rigid Body Dynamics*: Simulation of solid objects with mass, velocity, and rotational properties. +* *Collision Detection*: Determining when objects intersect or contact each other. +* *Collision Response*: Calculating how objects should react when they collide. +* *Constraints*: Limiting the movement of objects based on joints, hinges, or other connections. +* *Continuous Collision Detection*: Handling fast-moving objects that might pass through others between frames. +* *Spatial Partitioning*: Optimizing collision detection by dividing the world into regions. + +Let's implement a simple physics system that covers these basics, using a modern C++ approach consistent with our engine's design. + +=== Basic Physics System Implementation + +We'll start by defining the core classes for our physics system: + +[source,cpp] +---- +// Physics.h +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Engine { +namespace Physics { + +enum class ColliderType { + Box, + Sphere, + Capsule, + Mesh +}; + +class Collider { +public: + virtual ~Collider() = default; + virtual ColliderType GetType() const = 0; + + void SetOffset(const glm::vec3& offset) { m_Offset = offset; } + const glm::vec3& GetOffset() const { return m_Offset; } + +protected: + glm::vec3 m_Offset = glm::vec3(0.0f); +}; + +class BoxCollider : public Collider { +public: + BoxCollider(const glm::vec3& halfExtents) : m_HalfExtents(halfExtents) {} + + ColliderType GetType() const override { return ColliderType::Box; } + + const glm::vec3& GetHalfExtents() const { return m_HalfExtents; } + void SetHalfExtents(const glm::vec3& halfExtents) { m_HalfExtents = halfExtents; } + +private: + glm::vec3 m_HalfExtents; +}; + +class SphereCollider : public Collider { +public: + SphereCollider(float radius) : m_Radius(radius) {} + + ColliderType GetType() const override { return ColliderType::Sphere; } + + float GetRadius() const { return m_Radius; } + void SetRadius(float radius) { m_Radius = radius; } + +private: + float m_Radius; +}; + +class RigidBody { +public: + RigidBody(); + ~RigidBody(); + + // Kinematic state + void SetPosition(const glm::vec3& position) { m_Position = position; } + void SetRotation(const glm::quat& rotation) { m_Rotation = rotation; } + void SetLinearVelocity(const glm::vec3& velocity) { m_LinearVelocity = velocity; } + void SetAngularVelocity(const glm::vec3& velocity) { m_AngularVelocity = velocity; } + + const glm::vec3& GetPosition() const { return m_Position; } + const glm::quat& GetRotation() const { return m_Rotation; } + const glm::vec3& GetLinearVelocity() const { return m_LinearVelocity; } + const glm::vec3& GetAngularVelocity() const { return m_AngularVelocity; } + + // Physical properties + void SetMass(float mass); + float GetMass() const { return m_Mass; } + float GetInverseMass() const { return m_InverseMass; } + + void SetRestitution(float restitution) { m_Restitution = restitution; } + float GetRestitution() const { return m_Restitution; } + + void SetFriction(float friction) { m_Friction = friction; } + float GetFriction() const { return m_Friction; } + + // Collider management + void SetCollider(std::shared_ptr collider) { m_Collider = collider; } + std::shared_ptr GetCollider() const { return m_Collider; } + + // Forces and impulses + void ApplyForce(const glm::vec3& force); + void ApplyImpulse(const glm::vec3& impulse); + void ApplyTorque(const glm::vec3& torque); + void ApplyTorqueImpulse(const glm::vec3& torqueImpulse); + + // Simulation flags + void SetKinematic(bool kinematic) { m_IsKinematic = kinematic; } + bool IsKinematic() const { return m_IsKinematic; } + + void SetGravityEnabled(bool enabled) { m_UseGravity = enabled; } + bool IsGravityEnabled() const { return m_UseGravity; } + +private: + // Kinematic state + glm::vec3 m_Position = glm::vec3(0.0f); + glm::quat m_Rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 m_LinearVelocity = glm::vec3(0.0f); + glm::vec3 m_AngularVelocity = glm::vec3(0.0f); + + // Forces + glm::vec3 m_AccumulatedForce = glm::vec3(0.0f); + glm::vec3 m_AccumulatedTorque = glm::vec3(0.0f); + + // Physical properties + float m_Mass = 1.0f; + float m_InverseMass = 1.0f; + glm::mat3 m_InertiaTensor = glm::mat3(1.0f); + glm::mat3 m_InverseInertiaTensor = glm::mat3(1.0f); + float m_Restitution = 0.5f; + float m_Friction = 0.5f; + + // Collision + std::shared_ptr m_Collider; + + // Flags + bool m_IsKinematic = false; + bool m_UseGravity = true; + + // Update inertia tensor based on mass and collider + void UpdateInertiaTensor(); + + friend class PhysicsSystem; +}; + +struct CollisionInfo { + std::shared_ptr bodyA; + std::shared_ptr bodyB; + glm::vec3 contactPoint; + glm::vec3 normal; + float penetrationDepth; +}; + +class PhysicsSystem { +public: + PhysicsSystem(); + ~PhysicsSystem(); + + void Initialize(); + void Shutdown(); + + // Update physics simulation + void Update(float deltaTime); + + // RigidBody management + std::shared_ptr CreateRigidBody(); + void DestroyRigidBody(std::shared_ptr body); + + // World settings + void SetGravity(const glm::vec3& gravity) { m_Gravity = gravity; } + const glm::vec3& GetGravity() const { return m_Gravity; } + + // Collision detection + bool Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, RaycastHit& hit); + +private: + std::vector> m_RigidBodies; + glm::vec3 m_Gravity = glm::vec3(0.0f, -9.81f, 0.0f); + + // Simulation steps + void IntegrateForces(RigidBody& body, float deltaTime); + void IntegrateVelocities(RigidBody& body, float deltaTime); + + // Collision detection and response + void DetectCollisions(std::vector& collisions); + void ResolveCollisions(std::vector& collisions); + + // Helper functions for collision detection + bool CheckCollision(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); + bool SphereVsSphere(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); + bool BoxVsBox(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); + bool SphereVsBox(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); +}; + +struct RaycastHit { + std::shared_ptr body; + glm::vec3 point; + glm::vec3 normal; + float distance; +}; + +} // namespace Physics +} // namespace Engine +---- + +This basic structure provides a foundation for simulating rigid body physics with collision detection and response. In a real implementation, you would likely use a physics library like Bullet, PhysX, or Havok for more advanced features and optimizations. + +=== Integrating with the Engine + +To integrate our physics system with the rest of our engine, we'll add it to our engine's main class: + +[source,cpp] +---- +// Engine.h +#include "Physics.h" + +namespace Engine { + +class Engine { +public: + // ... existing engine code ... + + Physics::PhysicsSystem& GetPhysicsSystem() { return m_PhysicsSystem; } + +private: + // ... existing engine members ... + + Physics::PhysicsSystem m_PhysicsSystem; +}; + +} // namespace Engine +---- + +And we'll initialize it during engine startup: + +[source,cpp] +---- +// Engine.cpp +void Engine::Initialize() { + // ... existing initialization code ... + + m_PhysicsSystem.Initialize(); +} + +void Engine::Shutdown() { + m_PhysicsSystem.Shutdown(); + + // ... existing shutdown code ... +} +---- + +=== Basic Implementation of Physics Simulation + +Let's implement the core physics simulation functions: + +[source,cpp] +---- +// Physics.cpp +#include "Physics.h" + +namespace Engine { +namespace Physics { + +void PhysicsSystem::Update(float deltaTime) { + // Fixed timestep for stability + const float fixedTimeStep = 1.0f / 60.0f; + + // Accumulate forces (e.g., gravity) + for (auto& body : m_RigidBodies) { + if (!body->IsKinematic() && body->IsGravityEnabled()) { + body->m_AccumulatedForce += m_Gravity * body->m_Mass; + } + } + + // Integrate forces + for (auto& body : m_RigidBodies) { + if (!body->IsKinematic()) { + IntegrateForces(*body, fixedTimeStep); + } + } + + // Detect and resolve collisions + std::vector collisions; + DetectCollisions(collisions); + ResolveCollisions(collisions); + + // Integrate velocities + for (auto& body : m_RigidBodies) { + if (!body->IsKinematic()) { + IntegrateVelocities(*body, fixedTimeStep); + } + } + + // Clear accumulated forces + for (auto& body : m_RigidBodies) { + body->m_AccumulatedForce = glm::vec3(0.0f); + body->m_AccumulatedTorque = glm::vec3(0.0f); + } +} + +void PhysicsSystem::IntegrateForces(RigidBody& body, float deltaTime) { + // Update linear velocity + body.m_LinearVelocity += (body.m_AccumulatedForce * body.m_InverseMass) * deltaTime; + + // Update angular velocity + body.m_AngularVelocity += glm::vec3(body.m_InverseInertiaTensor * glm::vec4(body.m_AccumulatedTorque, 0.0f)) * deltaTime; + + // Apply damping + const float linearDamping = 0.01f; + const float angularDamping = 0.01f; + body.m_LinearVelocity *= (1.0f - linearDamping); + body.m_AngularVelocity *= (1.0f - angularDamping); +} + +void PhysicsSystem::IntegrateVelocities(RigidBody& body, float deltaTime) { + // Update position + body.m_Position += body.m_LinearVelocity * deltaTime; + + // Update rotation + glm::quat angularVelocityQuat(0.0f, body.m_AngularVelocity.x, body.m_AngularVelocity.y, body.m_AngularVelocity.z); + body.m_Rotation += (angularVelocityQuat * body.m_Rotation) * 0.5f * deltaTime; + body.m_Rotation = glm::normalize(body.m_Rotation); +} + +void PhysicsSystem::DetectCollisions(std::vector& collisions) { + // Simple O(n²) collision detection + for (size_t i = 0; i < m_RigidBodies.size(); i++) { + for (size_t j = i + 1; j < m_RigidBodies.size(); j++) { + auto& bodyA = m_RigidBodies[i]; + auto& bodyB = m_RigidBodies[j]; + + // Skip if both bodies are kinematic + if (bodyA->IsKinematic() && bodyB->IsKinematic()) { + continue; + } + + // Skip if either body doesn't have a collider + if (!bodyA->GetCollider() || !bodyB->GetCollider()) { + continue; + } + + CollisionInfo info; + if (CheckCollision(*bodyA, *bodyB, info)) { + info.bodyA = bodyA; + info.bodyB = bodyB; + collisions.push_back(info); + } + } + } +} + +void PhysicsSystem::ResolveCollisions(std::vector& collisions) { + for (auto& collision : collisions) { + auto bodyA = collision.bodyA; + auto bodyB = collision.bodyB; + + // Calculate relative velocity + glm::vec3 relativeVelocity = bodyB->m_LinearVelocity - bodyA->m_LinearVelocity; + + // Calculate impulse magnitude + float velocityAlongNormal = glm::dot(relativeVelocity, collision.normal); + + // Don't resolve if velocities are separating + if (velocityAlongNormal > 0) { + continue; + } + + // Calculate restitution (bounciness) + float restitution = std::min(bodyA->m_Restitution, bodyB->m_Restitution); + + // Calculate impulse scalar + float j = -(1.0f + restitution) * velocityAlongNormal; + j /= bodyA->m_InverseMass + bodyB->m_InverseMass; + + // Apply impulse + glm::vec3 impulse = collision.normal * j; + + if (!bodyA->IsKinematic()) { + bodyA->m_LinearVelocity -= impulse * bodyA->m_InverseMass; + } + + if (!bodyB->IsKinematic()) { + bodyB->m_LinearVelocity += impulse * bodyB->m_InverseMass; + } + + // Resolve penetration (position correction) + const float percent = 0.2f; // usually 20% to 80% + const float slop = 0.01f; // small penetration allowed + glm::vec3 correction = std::max(collision.penetrationDepth - slop, 0.0f) * percent * collision.normal / (bodyA->m_InverseMass + bodyB->m_InverseMass); + + if (!bodyA->IsKinematic()) { + bodyA->m_Position -= correction * bodyA->m_InverseMass; + } + + if (!bodyB->IsKinematic()) { + bodyB->m_Position += correction * bodyB->m_InverseMass; + } + } +} + +bool PhysicsSystem::CheckCollision(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info) { + auto colliderA = bodyA.GetCollider(); + auto colliderB = bodyB.GetCollider(); + + if (colliderA->GetType() == ColliderType::Sphere && colliderB->GetType() == ColliderType::Sphere) { + return SphereVsSphere(bodyA, bodyB, info); + } + else if (colliderA->GetType() == ColliderType::Box && colliderB->GetType() == ColliderType::Box) { + return BoxVsBox(bodyA, bodyB, info); + } + else if (colliderA->GetType() == ColliderType::Sphere && colliderB->GetType() == ColliderType::Box) { + return SphereVsBox(bodyA, bodyB, info); + } + else if (colliderA->GetType() == ColliderType::Box && colliderB->GetType() == ColliderType::Sphere) { + bool result = SphereVsBox(bodyB, bodyA, info); + if (result) { + // Flip normal direction + info.normal = -info.normal; + } + return result; + } + + // Unsupported collision types + return false; +} + +bool PhysicsSystem::SphereVsSphere(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info) { + auto sphereA = std::static_pointer_cast(bodyA.GetCollider()); + auto sphereB = std::static_pointer_cast(bodyB.GetCollider()); + + glm::vec3 posA = bodyA.GetPosition() + sphereA->GetOffset(); + glm::vec3 posB = bodyB.GetPosition() + sphereB->GetOffset(); + + float radiusA = sphereA->GetRadius(); + float radiusB = sphereB->GetRadius(); + + glm::vec3 direction = posB - posA; + float distance = glm::length(direction); + float minDistance = radiusA + radiusB; + + if (distance >= minDistance) { + return false; + } + + // Normalize direction + direction = distance > 0.0001f ? direction / distance : glm::vec3(0, 1, 0); + + info.contactPoint = posA + direction * radiusA; + info.normal = direction; + info.penetrationDepth = minDistance - distance; + + return true; +} + +// Implementation of BoxVsBox and SphereVsBox collision detection would go here +// These are more complex and would require additional helper functions + +} // namespace Physics +} // namespace Engine +---- + +=== Basic Usage Example + +Here's how you might use this physics system in a game: + +[source,cpp] +---- +// Game code +void Game::Initialize() { + // Create a ground plane + auto ground = m_Engine.GetPhysicsSystem().CreateRigidBody(); + ground->SetPosition(glm::vec3(0.0f, -1.0f, 0.0f)); + ground->SetKinematic(true); // Static object + auto groundCollider = std::make_shared(glm::vec3(50.0f, 1.0f, 50.0f)); + ground->SetCollider(groundCollider); + + // Create a dynamic box + auto box = m_Engine.GetPhysicsSystem().CreateRigidBody(); + box->SetPosition(glm::vec3(0.0f, 5.0f, 0.0f)); + box->SetMass(1.0f); + auto boxCollider = std::make_shared(glm::vec3(0.5f, 0.5f, 0.5f)); + box->SetCollider(boxCollider); + + // Create a dynamic sphere + auto sphere = m_Engine.GetPhysicsSystem().CreateRigidBody(); + sphere->SetPosition(glm::vec3(1.0f, 10.0f, 0.0f)); + sphere->SetMass(2.0f); + auto sphereCollider = std::make_shared(0.7f); + sphere->SetCollider(sphereCollider); + + // Store references to our objects + m_PhysicsObjects.push_back(ground); + m_PhysicsObjects.push_back(box); + m_PhysicsObjects.push_back(sphere); +} + +void Game::Update(float deltaTime) { + // Update physics + m_Engine.GetPhysicsSystem().Update(deltaTime); + + // Update visual representations of physics objects + for (auto& physicsObject : m_PhysicsObjects) { + auto visualObject = m_PhysicsToVisualMap[physicsObject]; + if (visualObject) { + visualObject->SetPosition(physicsObject->GetPosition()); + visualObject->SetRotation(physicsObject->GetRotation()); + } + } +} + +void Game::OnExplosion(const glm::vec3& position, float force) { + // Apply radial impulse to nearby objects + for (auto& physicsObject : m_PhysicsObjects) { + if (!physicsObject->IsKinematic()) { + glm::vec3 direction = physicsObject->GetPosition() - position; + float distance = glm::length(direction); + + if (distance < 10.0f) { + direction = glm::normalize(direction); + float impulseMagnitude = force * (1.0f - distance / 10.0f); + physicsObject->ApplyImpulse(direction * impulseMagnitude); + } + } + } +} +---- + +=== Limitations of Basic Physics Systems + +While this basic physics system provides the essential functionality for simulating rigid bodies in a game, it has several limitations: + +1. *Performance*: The O(n²) collision detection becomes a bottleneck with many objects. +2. *Limited Collision Shapes*: We've only implemented basic shapes like boxes and spheres. +3. *Stability Issues*: Simple integrators and collision resolution can lead to instability. +4. *No Continuous Collision Detection*: Fast-moving objects might tunnel through thin obstacles. +5. *Limited Constraints*: We haven't implemented joints, springs, or other constraints. +6. *CPU-Bound Processing*: All calculations are performed on the CPU, limiting scalability. + +In the next section, we'll explore how Vulkan compute shaders can address these limitations by offloading physics calculations to the GPU, particularly for large-scale simulations with many objects. + +link:03_vulkan_audio.adoc[Previous: Vulkan for Audio Processing] | link:05_vulkan_physics.adoc[Next: Vulkan for Physics Simulation] diff --git a/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc b/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc new file mode 100644 index 00000000..945a832d --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc @@ -0,0 +1,773 @@ +::pp: {plus}{plus} + += Subsystems: Vulkan for Physics Simulation +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Enhancing Physics with Vulkan + +In the previous section, we implemented a basic physics system for our engine. Now, we'll explore how Vulkan's compute capabilities can enhance physics simulations, particularly for large-scale scenarios with many interacting objects. + +=== Why Use Vulkan for Physics? + +Traditional physics simulations are performed on the CPU, but there are several compelling reasons to leverage Vulkan compute shaders for physics calculations: + +1. *Parallelism*: Physics calculations for multiple objects can be performed in parallel, making them well-suited for GPU computation. +2. *Scalability*: GPU-based physics can handle thousands or even millions of objects with relatively little performance degradation. +3. *Reduced CPU Load*: Offloading physics to the GPU frees up CPU resources for game logic, AI, and other tasks. +4. *Unified Memory*: With Vulkan, we can share memory between physics and graphics, reducing data transfer overhead. +5. *Specialized Hardware*: Modern GPUs often include hardware features specifically designed to accelerate physics-like calculations. + +=== Common GPU Physics Applications + +While not all physics calculations are suitable for GPU acceleration, several common physics tasks can benefit significantly: + +1. *Particle Systems*: Simulating thousands of particles for effects like smoke, fire, or fluid. +2. *Cloth Simulation*: Calculating the behavior of cloth, hair, or other deformable objects. +3. *Soft Body Physics*: Simulating objects that can bend, stretch, or compress. +4. *Broad-Phase Collision Detection*: Quickly identifying potential collision pairs among many objects. +5. *Rigid Body Dynamics*: Simulating the movement of large numbers of rigid bodies. + +Let's focus on implementing GPU-accelerated rigid body dynamics and collision detection using Vulkan compute shaders. + +=== GPU-Accelerated Rigid Body Physics + +To implement GPU-accelerated physics, we'll need to: + +1. Store physics data in GPU-accessible buffers +2. Create compute shaders to perform physics calculations +3. Integrate the GPU physics with our existing CPU-based system + +Let's extend our physics system to include Vulkan-accelerated components: + +[source,cpp] +---- +// Physics.h (additions) +#include + +namespace Engine { +namespace Physics { + +// Structure for GPU physics data +struct GPUPhysicsData { + glm::vec4 position; // xyz = position, w = inverse mass + glm::vec4 rotation; // quaternion + glm::vec4 linearVelocity; // xyz = velocity, w = restitution + glm::vec4 angularVelocity; // xyz = angular velocity, w = friction + glm::vec4 force; // xyz = force, w = is kinematic (0 or 1) + glm::vec4 torque; // xyz = torque, w = use gravity (0 or 1) + glm::vec4 colliderData; // type-specific data (e.g., radius for spheres) + glm::vec4 colliderData2; // additional collider data (e.g., box half extents) +}; + +// Structure for GPU collision data +struct GPUCollisionData { + uint32_t bodyA; + uint32_t bodyB; + glm::vec4 contactNormal; // xyz = normal, w = penetration depth + glm::vec4 contactPoint; // xyz = contact point, w = unused +}; + +// Extended PhysicsSystem with Vulkan acceleration +class PhysicsSystem { +public: + // ... existing methods ... + + // Enable/disable GPU acceleration + void SetGPUAccelerationEnabled(bool enabled) { m_GPUAccelerationEnabled = enabled; } + bool IsGPUAccelerationEnabled() const { return m_GPUAccelerationEnabled; } + + // Set the maximum number of objects that can be simulated on the GPU + void SetMaxGPUObjects(uint32_t maxObjects); + +private: + // ... existing members ... + + // GPU acceleration + bool m_GPUAccelerationEnabled = false; + uint32_t m_MaxGPUObjects = 1024; + uint32_t m_MaxGPUCollisions = 4096; + + // Vulkan resources for physics simulation + struct VulkanResources { + // Shader modules + vk::raii::ShaderModule integrateShaderModule = nullptr; + vk::raii::ShaderModule broadPhaseShaderModule = nullptr; + vk::raii::ShaderModule narrowPhaseShaderModule = nullptr; + vk::raii::ShaderModule resolveShaderModule = nullptr; + + // Pipeline layouts and compute pipelines + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline integratePipeline = nullptr; + vk::raii::Pipeline broadPhasePipeline = nullptr; + vk::raii::Pipeline narrowPhasePipeline = nullptr; + vk::raii::Pipeline resolvePipeline = nullptr; + + // Descriptor pool and sets + vk::raii::DescriptorPool descriptorPool = nullptr; + std::vector descriptorSets; + + // Buffers for physics data + vk::raii::Buffer physicsBuffer = nullptr; + vk::raii::DeviceMemory physicsBufferMemory = nullptr; + vk::raii::Buffer collisionBuffer = nullptr; + vk::raii::DeviceMemory collisionBufferMemory = nullptr; + vk::raii::Buffer pairBuffer = nullptr; + vk::raii::DeviceMemory pairBufferMemory = nullptr; + vk::raii::Buffer counterBuffer = nullptr; + vk::raii::DeviceMemory counterBufferMemory = nullptr; + + // Command buffer for compute operations + vk::raii::CommandPool commandPool = nullptr; + vk::raii::CommandBuffer commandBuffer = nullptr; + }; + + VulkanResources m_VulkanResources; + + // Initialize Vulkan resources for physics simulation + void InitializeVulkanResources(); + void CleanupVulkanResources(); + + // Update physics data on the GPU + void UpdateGPUPhysicsData(); + + // Read back physics data from the GPU + void ReadbackGPUPhysicsData(); + + // Perform GPU-accelerated physics simulation + void SimulatePhysicsOnGPU(float deltaTime); +}; + +} // namespace Physics +} // namespace Engine +---- + +Now, let's implement the Vulkan-based physics simulation: + +[source,cpp] +---- +// Physics.cpp (implementation) + +void PhysicsSystem::InitializeVulkanResources() { + // Get Vulkan device from the engine + auto& device = m_Engine.GetVulkanDevice(); + + // Create compute shader modules + auto integrateShaderCode = LoadShaderFile("shaders/physics_integrate.comp.spv"); + vk::ShaderModuleCreateInfo integrateShaderModuleCreateInfo({}, integrateShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(integrateShaderCode.data())); + m_VulkanResources.integrateShaderModule = vk::raii::ShaderModule(device, integrateShaderModuleCreateInfo); + + auto broadPhaseShaderCode = LoadShaderFile("shaders/physics_broad_phase.comp.spv"); + vk::ShaderModuleCreateInfo broadPhaseShaderModuleCreateInfo({}, broadPhaseShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(broadPhaseShaderCode.data())); + m_VulkanResources.broadPhaseShaderModule = vk::raii::ShaderModule(device, broadPhaseShaderModuleCreateInfo); + + auto narrowPhaseShaderCode = LoadShaderFile("shaders/physics_narrow_phase.comp.spv"); + vk::ShaderModuleCreateInfo narrowPhaseShaderModuleCreateInfo({}, narrowPhaseShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(narrowPhaseShaderCode.data())); + m_VulkanResources.narrowPhaseShaderModule = vk::raii::ShaderModule(device, narrowPhaseShaderModuleCreateInfo); + + auto resolveShaderCode = LoadShaderFile("shaders/physics_resolve.comp.spv"); + vk::ShaderModuleCreateInfo resolveShaderModuleCreateInfo({}, resolveShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(resolveShaderCode.data())); + m_VulkanResources.resolveShaderModule = vk::raii::ShaderModule(device, resolveShaderModuleCreateInfo); + + // Create descriptor set layout + std::array bindings = { + // Physics data buffer + vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Collision data buffer + vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Pair buffer (for broad phase) + vk::DescriptorSetLayoutBinding(2, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Counter buffer + vk::DescriptorSetLayoutBinding(3, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute) + }; + + vk::DescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo({}, bindings); + m_VulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(device, descriptorSetLayoutCreateInfo); + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo({}, *m_VulkanResources.descriptorSetLayout); + m_VulkanResources.pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutCreateInfo); + + // Create compute pipelines + vk::PipelineShaderStageCreateInfo integrateShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.integrateShaderModule, "main"); + vk::ComputePipelineCreateInfo integrateComputePipelineCreateInfo({}, integrateShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.integratePipeline = vk::raii::Pipeline(device, nullptr, integrateComputePipelineCreateInfo); + + vk::PipelineShaderStageCreateInfo broadPhaseShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.broadPhaseShaderModule, "main"); + vk::ComputePipelineCreateInfo broadPhaseComputePipelineCreateInfo({}, broadPhaseShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.broadPhasePipeline = vk::raii::Pipeline(device, nullptr, broadPhaseComputePipelineCreateInfo); + + vk::PipelineShaderStageCreateInfo narrowPhaseShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.narrowPhaseShaderModule, "main"); + vk::ComputePipelineCreateInfo narrowPhaseComputePipelineCreateInfo({}, narrowPhaseShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.narrowPhasePipeline = vk::raii::Pipeline(device, nullptr, narrowPhaseComputePipelineCreateInfo); + + vk::PipelineShaderStageCreateInfo resolveShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.resolveShaderModule, "main"); + vk::ComputePipelineCreateInfo resolveComputePipelineCreateInfo({}, resolveShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.resolvePipeline = vk::raii::Pipeline(device, nullptr, resolveComputePipelineCreateInfo); + + // Create descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 4) + }; + vk::DescriptorPoolCreateInfo descriptorPoolCreateInfo({}, 1, poolSizes); + m_VulkanResources.descriptorPool = vk::raii::DescriptorPool(device, descriptorPoolCreateInfo); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo descriptorSetAllocateInfo(*m_VulkanResources.descriptorPool, + 1, &*m_VulkanResources.descriptorSetLayout); + m_VulkanResources.descriptorSets = vk::raii::DescriptorSets(device, descriptorSetAllocateInfo); + + // Create buffers for physics data + CreateBuffer(device, sizeof(GPUPhysicsData) * m_MaxGPUObjects, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.physicsBuffer, m_VulkanResources.physicsBufferMemory); + + CreateBuffer(device, sizeof(GPUCollisionData) * m_MaxGPUCollisions, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.collisionBuffer, m_VulkanResources.collisionBufferMemory); + + CreateBuffer(device, sizeof(uint32_t) * 2 * m_MaxGPUCollisions, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.pairBuffer, m_VulkanResources.pairBufferMemory); + + CreateBuffer(device, sizeof(uint32_t) * 2, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.counterBuffer, m_VulkanResources.counterBufferMemory); + + // Update descriptor sets + std::array bufferInfos = { + vk::DescriptorBufferInfo(*m_VulkanResources.physicsBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.collisionBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.pairBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.counterBuffer, 0, VK_WHOLE_SIZE) + }; + + std::array descriptorWrites = { + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 0, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[0]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 1, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[1]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 2, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[2]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 3, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[3]) + }; + + device.updateDescriptorSets(descriptorWrites, {}); + + // Create command pool and command buffer + vk::CommandPoolCreateInfo commandPoolCreateInfo({}, m_Engine.GetVulkanQueueFamilyIndex()); + m_VulkanResources.commandPool = vk::raii::CommandPool(device, commandPoolCreateInfo); + + vk::CommandBufferAllocateInfo commandBufferAllocateInfo(*m_VulkanResources.commandPool, + vk::CommandBufferLevel::ePrimary, 1); + auto commandBuffers = vk::raii::CommandBuffers(device, commandBufferAllocateInfo); + m_VulkanResources.commandBuffer = std::move(commandBuffers[0]); + + // Initialize counter buffer + uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count + void* data; + vkMapMemory(device, *m_VulkanResources.counterBufferMemory, 0, sizeof(initialCounters), 0, &data); + memcpy(data, initialCounters, sizeof(initialCounters)); + vkUnmapMemory(device, *m_VulkanResources.counterBufferMemory); +} + +void PhysicsSystem::UpdateGPUPhysicsData() { + auto& device = m_Engine.GetVulkanDevice(); + + // Map the physics buffer + void* data; + vkMapMemory(device, *m_VulkanResources.physicsBufferMemory, 0, + sizeof(GPUPhysicsData) * m_RigidBodies.size(), 0, &data); + + // Copy physics data to the buffer + GPUPhysicsData* gpuData = static_cast(data); + for (size_t i = 0; i < m_RigidBodies.size(); i++) { + auto& body = m_RigidBodies[i]; + + gpuData[i].position = glm::vec4(body->GetPosition(), body->GetInverseMass()); + gpuData[i].rotation = glm::vec4(body->GetRotation().x, body->GetRotation().y, + body->GetRotation().z, body->GetRotation().w); + gpuData[i].linearVelocity = glm::vec4(body->GetLinearVelocity(), body->GetRestitution()); + gpuData[i].angularVelocity = glm::vec4(body->GetAngularVelocity(), body->GetFriction()); + gpuData[i].force = glm::vec4(body->m_AccumulatedForce, body->IsKinematic() ? 1.0f : 0.0f); + gpuData[i].torque = glm::vec4(body->m_AccumulatedTorque, body->IsGravityEnabled() ? 1.0f : 0.0f); + + // Set collider data based on collider type + auto collider = body->GetCollider(); + if (collider) { + switch (collider->GetType()) { + case ColliderType::Sphere: { + auto sphereCollider = std::static_pointer_cast(collider); + gpuData[i].colliderData = glm::vec4(sphereCollider->GetRadius(), 0.0f, 0.0f, + static_cast(ColliderType::Sphere)); + gpuData[i].colliderData2 = glm::vec4(collider->GetOffset(), 0.0f); + break; + } + case ColliderType::Box: { + auto boxCollider = std::static_pointer_cast(collider); + gpuData[i].colliderData = glm::vec4(boxCollider->GetHalfExtents(), + static_cast(ColliderType::Box)); + gpuData[i].colliderData2 = glm::vec4(collider->GetOffset(), 0.0f); + break; + } + default: + // Unsupported collider type + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + } + } else { + // No collider + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); + gpuData[i].colliderData2 = glm::vec4(0.0f); + } + } + + vkUnmapMemory(device, *m_VulkanResources.physicsBufferMemory); + + // Reset counters + uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count + vkMapMemory(device, *m_VulkanResources.counterBufferMemory, 0, sizeof(initialCounters), 0, &data); + memcpy(data, initialCounters, sizeof(initialCounters)); + vkUnmapMemory(device, *m_VulkanResources.counterBufferMemory); +} + +void PhysicsSystem::ReadbackGPUPhysicsData() { + auto& device = m_Engine.GetVulkanDevice(); + + // Map the physics buffer + void* data; + vkMapMemory(device, *m_VulkanResources.physicsBufferMemory, 0, + sizeof(GPUPhysicsData) * m_RigidBodies.size(), 0, &data); + + // Copy physics data from the buffer + GPUPhysicsData* gpuData = static_cast(data); + for (size_t i = 0; i < m_RigidBodies.size(); i++) { + auto& body = m_RigidBodies[i]; + + // Skip kinematic bodies + if (body->IsKinematic()) { + continue; + } + + body->SetPosition(glm::vec3(gpuData[i].position)); + body->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, + gpuData[i].rotation.y, gpuData[i].rotation.z)); + body->SetLinearVelocity(glm::vec3(gpuData[i].linearVelocity)); + body->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); + } + + vkUnmapMemory(device, *m_VulkanResources.physicsBufferMemory); +} + +void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { + auto& device = m_Engine.GetVulkanDevice(); + auto& queue = m_Engine.GetVulkanComputeQueue(); + + // Update physics data on the GPU + UpdateGPUPhysicsData(); + + // Record command buffer + vk::CommandBufferBeginInfo beginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); + m_VulkanResources.commandBuffer.begin(beginInfo); + + // Bind descriptor set + m_VulkanResources.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.pipelineLayout, 0, + *m_VulkanResources.descriptorSets[0], {}); + + // Push constants for simulation parameters + struct { + float deltaTime; + float gravity[3]; + uint32_t numBodies; + } pushConstants; + + pushConstants.deltaTime = deltaTime; + pushConstants.gravity[0] = m_Gravity.x; + pushConstants.gravity[1] = m_Gravity.y; + pushConstants.gravity[2] = m_Gravity.z; + pushConstants.numBodies = static_cast(m_RigidBodies.size()); + + m_VulkanResources.commandBuffer.pushConstants(*m_VulkanResources.pipelineLayout, + vk::ShaderStageFlagBits::eCompute, 0, + sizeof(pushConstants), &pushConstants); + + // Step 1: Integrate forces and velocities + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.integratePipeline); + m_VulkanResources.commandBuffer.dispatch((pushConstants.numBodies + 63) / 64, 1, 1); + + // Memory barrier to ensure integration is complete before collision detection + vk::MemoryBarrier memoryBarrier(vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eShaderRead); + m_VulkanResources.commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, memoryBarrier, {}, {}); + + // Step 2: Broad-phase collision detection + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.broadPhasePipeline); + // Each thread checks one pair of objects + uint32_t numPairs = (pushConstants.numBodies * (pushConstants.numBodies - 1)) / 2; + m_VulkanResources.commandBuffer.dispatch((numPairs + 63) / 64, 1, 1); + + // Memory barrier to ensure broad phase is complete before narrow phase + m_VulkanResources.commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, memoryBarrier, {}, {}); + + // Step 3: Narrow-phase collision detection + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.narrowPhasePipeline); + // We don't know how many pairs were generated, so we use a conservative estimate + m_VulkanResources.commandBuffer.dispatch((m_MaxGPUCollisions + 63) / 64, 1, 1); + + // Memory barrier to ensure narrow phase is complete before resolution + m_VulkanResources.commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, memoryBarrier, {}, {}); + + // Step 4: Collision resolution + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.resolvePipeline); + // We don't know how many collisions were detected, so we use a conservative estimate + m_VulkanResources.commandBuffer.dispatch((m_MaxGPUCollisions + 63) / 64, 1, 1); + + m_VulkanResources.commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo({}, {}, *m_VulkanResources.commandBuffer); + queue.submit(submitInfo, nullptr); + queue.waitIdle(); + + // Read back physics data from the GPU + ReadbackGPUPhysicsData(); +} + +void PhysicsSystem::Update(float deltaTime) { + if (m_GPUAccelerationEnabled && m_RigidBodies.size() <= m_MaxGPUObjects) { + // Use GPU-accelerated physics + SimulatePhysicsOnGPU(deltaTime); + } else { + // Fall back to CPU physics + // ... existing CPU physics code ... + } +} +---- + +=== Physics Compute Shaders + +Now, let's implement the compute shaders for our GPU-accelerated physics system: + +[source,glsl] +---- +// physics_integrate.comp +#version 450 + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +// Push constants +layout(push_constant) uniform PushConstants { + float deltaTime; + vec3 gravity; + uint numBodies; +} pushConstants; + +// Physics data +struct PhysicsData { + vec4 position; // xyz = position, w = inverse mass + vec4 rotation; // quaternion + vec4 linearVelocity; // xyz = velocity, w = restitution + vec4 angularVelocity; // xyz = angular velocity, w = friction + vec4 force; // xyz = force, w = is kinematic (0 or 1) + vec4 torque; // xyz = torque, w = use gravity (0 or 1) + vec4 colliderData; // type-specific data (e.g., radius for spheres) + vec4 colliderData2; // additional collider data (e.g., box half extents) +}; + +layout(std430, binding = 0) buffer PhysicsBuffer { + PhysicsData bodies[]; +} physicsBuffer; + +// Quaternion multiplication +vec4 quatMul(vec4 q1, vec4 q2) { + return vec4( + q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + ); +} + +// Quaternion normalization +vec4 quatNormalize(vec4 q) { + float len = length(q); + if (len > 0.0001) { + return q / len; + } + return vec4(0, 0, 0, 1); +} + +void main() { + uint gID = gl_GlobalInvocationID.x; + + // Check if this invocation is within the number of bodies + if (gID >= pushConstants.numBodies) { + return; + } + + // Get physics data for this body + PhysicsData body = physicsBuffer.bodies[gID]; + + // Skip kinematic bodies + if (body.force.w > 0.5) { + return; + } + + // Apply gravity if enabled + if (body.torque.w > 0.5) { + body.force.xyz += pushConstants.gravity / body.position.w; + } + + // Integrate forces + body.linearVelocity.xyz += body.force.xyz * body.position.w * pushConstants.deltaTime; + body.angularVelocity.xyz += body.torque.xyz * pushConstants.deltaTime; // Simplified, should use inertia tensor + + // Apply damping + const float linearDamping = 0.01; + const float angularDamping = 0.01; + body.linearVelocity.xyz *= (1.0 - linearDamping); + body.angularVelocity.xyz *= (1.0 - angularDamping); + + // Integrate velocities + body.position.xyz += body.linearVelocity.xyz * pushConstants.deltaTime; + + // Update rotation + vec4 angularVelocityQuat = vec4(body.angularVelocity.xyz * 0.5, 0.0); + vec4 rotationDelta = quatMul(angularVelocityQuat, body.rotation); + body.rotation = quatNormalize(body.rotation + rotationDelta * pushConstants.deltaTime); + + // Write updated data back to buffer + physicsBuffer.bodies[gID] = body; +} +---- + +[source,glsl] +---- +// physics_broad_phase.comp +#version 450 + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +// Push constants +layout(push_constant) uniform PushConstants { + float deltaTime; + vec3 gravity; + uint numBodies; +} pushConstants; + +// Physics data +struct PhysicsData { + vec4 position; // xyz = position, w = inverse mass + vec4 rotation; // quaternion + vec4 linearVelocity; // xyz = velocity, w = restitution + vec4 angularVelocity; // xyz = angular velocity, w = friction + vec4 force; // xyz = force, w = is kinematic (0 or 1) + vec4 torque; // xyz = torque, w = use gravity (0 or 1) + vec4 colliderData; // type-specific data (e.g., radius for spheres) + vec4 colliderData2; // additional collider data (e.g., box half extents) +}; + +layout(std430, binding = 0) buffer PhysicsBuffer { + PhysicsData bodies[]; +} physicsBuffer; + +// Pair buffer for potential collisions +layout(std430, binding = 2) buffer PairBuffer { + uvec2 pairs[]; +} pairBuffer; + +// Counter buffer +layout(std430, binding = 3) buffer CounterBuffer { + uint pairCount; + uint collisionCount; +} counterBuffer; + +// Compute AABB for a body +void computeAABB(PhysicsData body, out vec3 min, out vec3 max) { + // Default to a small AABB + min = body.position.xyz - vec3(0.1); + max = body.position.xyz + vec3(0.1); + + // Check collider type + int colliderType = int(body.colliderData.w); + + if (colliderType == 0) { // Sphere + float radius = body.colliderData.x; + vec3 center = body.position.xyz + body.colliderData2.xyz; + min = center - vec3(radius); + max = center + vec3(radius); + } + else if (colliderType == 1) { // Box + vec3 halfExtents = body.colliderData.xyz; + vec3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } +} + +bool aabbOverlap(vec3 minA, vec3 maxA, vec3 minB, vec3 maxB) { + return all(lessThan(minA, maxB)) && all(lessThan(minB, maxA)); +} + +void main() { + uint gID = gl_GlobalInvocationID.x; + + // Calculate which pair of bodies this thread should check + uint numBodies = pushConstants.numBodies; + uint numPairs = (numBodies * (numBodies - 1)) / 2; + + if (gID >= numPairs) { + return; + } + + // Convert linear index to pair indices (i, j) where i < j + uint i = 0; + uint j = 0; + + // This is a mathematical formula to convert a linear index to a pair of indices + uint row = uint(floor(sqrt(float(2 * gID + 0.25)) - 0.5)); + i = row; + j = gID - (row * (row + 1)) / 2; + + // Ensure j > i + j += i + 1; + + // Get physics data for both bodies + PhysicsData bodyA = physicsBuffer.bodies[i]; + PhysicsData bodyB = physicsBuffer.bodies[j]; + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Skip if either body doesn't have a collider + if (bodyA.colliderData.w < 0 || bodyB.colliderData.w < 0) { + return; + } + + // Compute AABBs + vec3 minA, maxA, minB, maxB; + computeAABB(bodyA, minA, maxA); + computeAABB(bodyB, minB, maxB); + + // Check for AABB overlap + if (aabbOverlap(minA, maxA, minB, maxB)) { + // Add to potential collision pairs + uint pairIndex = atomicAdd(counterBuffer.pairCount, 1); + pairBuffer.pairs[pairIndex] = uvec2(i, j); + } +} +---- + +The narrow-phase and resolve shaders would follow a similar pattern, implementing the detailed collision detection and resolution algorithms. + +=== Performance Considerations + +When implementing GPU-accelerated physics with Vulkan, consider these performance optimizations: + +1. *Batch Processing*: Process multiple physics steps in a single dispatch to amortize the overhead of command submission. +2. *Memory Transfers*: Minimize transfers between CPU and GPU memory by keeping physics data on the GPU when possible. +3. *Spatial Partitioning*: Implement grid or tree-based spatial partitioning to reduce the number of potential collision pairs. +4. *Workgroup Size*: Tune the workgroup size based on your target hardware for optimal performance. +5. *Memory Layout*: Organize physics data for optimal cache coherency on the GPU. + +=== Integration with the Engine + +To integrate the GPU-accelerated physics into our engine, we need to modify the `PhysicsSystem::Initialize` method: + +[source,cpp] +---- +void PhysicsSystem::Initialize() { + // Initialize basic physics system + // ... + + // Initialize Vulkan resources for GPU-accelerated physics + if (m_Engine.IsVulkanInitialized()) { + InitializeVulkanResources(); + m_GPUAccelerationEnabled = true; + } +} + +void PhysicsSystem::Shutdown() { + // Cleanup Vulkan resources + if (m_Engine.IsVulkanInitialized()) { + CleanupVulkanResources(); + } + + // Shutdown basic physics system + // ... +} +---- + +=== Advantages of Vulkan-Based Physics + +By implementing physics simulation with Vulkan compute shaders, we gain several advantages: + +1. *Scalability*: The GPU can simulate thousands or even millions of objects in parallel. +2. *Performance*: GPU-accelerated physics can be orders of magnitude faster than CPU-based solutions for large-scale simulations. +3. *CPU Offloading*: Physics processing no longer competes with game logic for CPU resources. +4. *Advanced Simulations*: The GPU's computational power enables more complex physics simulations like fluid dynamics or cloth. + +=== Limitations and Considerations + +While Vulkan-based physics offers many advantages, there are some limitations to consider: + +1. *Complexity*: Implementing and debugging GPU-based physics is more complex than CPU-based solutions. +2. *Precision*: GPUs typically use single-precision floating-point, which may lead to numerical stability issues in some simulations. +3. *Platform Support*: Not all platforms support Vulkan, so you may need fallback CPU implementations. +4. *Synchronization*: Keeping CPU and GPU physics data in sync can be challenging and may introduce latency. + +=== Real-World Applications + +Several modern game engines and physics middleware solutions leverage GPU acceleration for physics simulations: + +1. *NVIDIA PhysX*: Supports GPU acceleration for certain physics calculations. +2. *Bullet Physics*: Has experimental GPU acceleration using compute shaders. +3. *Flex*: NVIDIA's particle-based physics solver designed specifically for GPU acceleration. +4. *Custom Solutions*: AAA game studios often implement custom GPU-accelerated physics for their titles. + +By implementing Vulkan-based physics in our engine, we're following industry best practices for high-performance physics in modern games. + +=== Conclusion + +In this chapter, we've explored how Vulkan compute shaders can be used to accelerate both audio and physics processing in a game engine. By leveraging the GPU's massive parallel processing capabilities, we can create more immersive and dynamic game worlds with realistic audio and physics simulations. + +The techniques we've covered demonstrate the versatility of Vulkan beyond traditional graphics rendering. As you continue to develop your engine, consider other areas where GPU acceleration might provide benefits, such as AI pathfinding, procedural generation, or particle systems. + +link:04_physics_basics.adoc[Previous: Physics Basics] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc new file mode 100644 index 00000000..50540c5e --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc @@ -0,0 +1,118 @@ +::pp: {plus}{plus} + += Subsystems: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've explored how to implement and enhance two critical engine subsystems—Audio and Physics—using Vulkan's compute capabilities. Let's summarize what we've learned and discuss potential future directions. + +=== What We've Learned + +==== Audio Subsystems + +We started by implementing a basic audio system that provides the foundation for sound playback in our engine. This system includes: + +* Audio resource management for loading and playing sound files +* Spatial audio positioning based on listener and source positions +* A flexible architecture that can be integrated with various audio backends + +We then enhanced this basic system with Vulkan compute shaders to implement Head-Related Transfer Function (HRTF) processing for more realistic 3D audio. This approach demonstrated: + +* How to offload computationally intensive audio processing to the GPU +* Techniques for implementing real-time convolution using compute shaders +* Methods for sharing data efficiently between CPU and GPU audio processing + +==== Physics Subsystems + +Similarly, we implemented a basic physics system that provides rigid body dynamics and collision detection. This system includes: + +* Rigid body simulation with forces, impulses, and collisions +* Various collider types for different geometric shapes +* Integration with the rest of our engine for visual representation of physics objects + +We then enhanced this system with Vulkan compute shaders to accelerate physics calculations, demonstrating: + +* Techniques for parallel physics simulation on the GPU +* Multi-stage physics processing (integration, broad phase, narrow phase, resolution) +* Methods for handling large numbers of physics objects efficiently + +==== Vulkan Integration + +Throughout both subsystems, we leveraged Vulkan's compute capabilities. We demonstrated: + +* Creating and managing compute pipelines for non-graphical tasks +* Efficient memory sharing between CPU and GPU +* Synchronization techniques for ensuring correct execution order +* Performance optimization strategies for compute shader workloads + +=== Potential Improvements + +While our implementations provide a solid foundation, there are several areas where they could be enhanced: + +==== Audio Improvements + +* *Advanced HRTF Models*: Implement more sophisticated HRTF models that account for individual differences in head and ear shapes. +* *Environmental Effects*: Add reverb, occlusion, and other environmental effects based on scene geometry. +* *Streaming Audio*: Implement streaming for large audio files to reduce memory usage. +* *Compression*: Add support for compressed audio formats to reduce memory and bandwidth requirements. +* *Voice Communication*: Integrate real-time voice processing for multiplayer games. + +==== Physics Improvements + +* *Advanced Collision Shapes*: Add support for more complex collision shapes like convex hulls and trimeshes. +* *Constraints and Joints*: Implement various types of constraints and joints for more complex mechanical systems. +* *Continuous Collision Detection*: Add support for detecting collisions between fast-moving objects. +* *Soft Body Physics*: Extend the system to support deformable objects like cloth, ropes, and soft bodies. +* *Fluid Simulation*: Implement fluid dynamics for realistic water, smoke, and fire effects. + +==== General Improvements + +* *Cross-Platform Support*: Ensure the subsystems work across different platforms, with appropriate fallbacks for devices without Vulkan support. +* *Profiling and Optimization*: Add detailed profiling to identify and address performance bottlenecks. +* *Memory Management*: Implement more sophisticated memory management to reduce fragmentation and improve cache coherency. +* *Multi-Threading*: Further optimize CPU-side processing with multi-threading where appropriate. + +=== Integration with Other Engine Systems + +As you continue developing your engine, consider how these subsystems interact with other components: + +* *Rendering System*: Visualize physics debug information, audio sources, and listener positions. +* *Animation System*: Synchronize animations with audio events and physics interactions. +* *Scripting System*: Provide high-level interfaces for controlling audio and physics from game scripts. +* *Networking*: Implement efficient synchronization of audio and physics state across networked clients. + +=== Real-World Considerations + +When using these subsystems in production applications, keep these considerations in mind: + +* *Performance Profiling*: Regularly profile your audio and physics systems to ensure they're not becoming bottlenecks. +* *Memory Usage*: Monitor memory usage, especially for large numbers of audio sources or physics objects. +* *Platform Differences*: Test on various hardware configurations to ensure consistent behavior. +* *Power Consumption*: Be mindful of power usage, especially on mobile devices where GPU compute can drain batteries quickly. + +=== Final Thoughts + +Audio and physics are essential components that contribute significantly to the immersion and interactivity of modern games. By leveraging Vulkan's compute capabilities, we can create more sophisticated and performant implementations of these subsystems, enabling richer and more dynamic game experiences. + +The techniques we've explored in this chapter demonstrate the versatility of Vulkan beyond traditional graphics rendering. As you continue to develop your engine, consider other areas where GPU acceleration might provide benefits, such as AI pathfinding, procedural generation, or particle systems. + +Remember that the implementations provided here are starting points. Real-world engines often require customization and optimization based on the specific needs of your games and target platforms. Don't hesitate to experiment and extend these systems to meet your unique requirements. + +=== Code Examples + +The complete code for this chapter can be found in the following files: + +* `simple_engine/30_audio_subsystem.cpp`: Implementation of the audio subsystem with Vulkan HRTF processing +* `simple_engine/31_physics_subsystem.cpp`: Implementation of the physics subsystem with Vulkan acceleration + +link:../../attachments/simple_engine/30_audio_subsystem.cpp[Audio Subsystem C{pp} code] +link:../../attachments/simple_engine/31_physics_subsystem.cpp[Physics Subsystem C{pp} code] + +link:05_vulkan_physics.adoc[Previous: Vulkan for Physics Simulation] | link:../Tooling/01_introduction.adoc[Next: Tooling] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Subsystems/index.adoc b/en/Building_a_Simple_Engine/Subsystems/index.adoc new file mode 100644 index 00000000..58f6923d --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/index.adoc @@ -0,0 +1,21 @@ +::pp: {plus}{plus} + += Subsystems +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +This chapter covers the implementation of critical engine subsystems - Audio and Physics - with a focus on leveraging Vulkan's compute capabilities for enhanced performance. + +* link:01_introduction.adoc[Introduction] +* link:02_audio_basics.adoc[Audio Basics] +* link:03_vulkan_audio.adoc[Vulkan for Audio Processing] +* link:04_physics_basics.adoc[Physics Basics] +* link:05_vulkan_physics.adoc[Vulkan for Physics Simulation] +* link:06_conclusion.adoc[Conclusion] + +link:../Loading_Models/09_conclusion.adoc[Previous: Loading Models Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc new file mode 100644 index 00000000..be03736e --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc @@ -0,0 +1,40 @@ +::pp: {plus}{plus} + += Tooling: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Introduction to Engine Tooling + +In previous chapters, we've built the foundation of our simple engine, implementing core components like the rendering pipeline, camera systems, model loading, and essential subsystems like audio and physics. Now, we're ready to explore the tooling ecosystem that supports the development, debugging, and distribution of a professional Vulkan application. + +Effective tooling is critical for maintaining productivity, ensuring quality, and delivering a robust final product. While these tools may seem separate from the engine itself, they are integral to the development process and can significantly impact the quality and maintainability of your code. + +=== What We'll Cover + +In this chapter, we'll explore: + +* *CI/CD for Vulkan Projects*: We'll implement a continuous integration and continuous deployment pipeline specifically tailored for Vulkan applications, ensuring consistent builds and automated testing across different platforms. + +* *Debugging with VK_KHR_debug_utils and RenderDoc*: We'll explore how to use Vulkan's debugging extensions and external tools like RenderDoc to identify and fix issues in your rendering pipeline. + +* *Crash Handling and Minidumps*: We'll implement robust crash handling mechanisms and learn how to generate and analyze minidumps to diagnose issues in production environments. + +* *Vulkan Extensions for Robustness*: We'll explore extensions like VK_EXT_robustness2 that can help make your application more resilient to undefined behavior. + +=== Prerequisites + +Before diving into this chapter, you should be familiar with: + +* The basics of Vulkan and our engine architecture from previous chapters +* Modern C++ concepts, particularly those introduced in C++17 and C++20 +* Basic understanding of software development workflows and tools + +Let's begin by exploring how to set up a CI/CD pipeline for Vulkan projects. + +link:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:02_cicd.adoc[Next: CI/CD for Vulkan Projects] diff --git a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc new file mode 100644 index 00000000..47f344ee --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc @@ -0,0 +1,246 @@ +::pp: {plus}{plus} + += Tooling: CI/CD for Vulkan Projects +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Continuous Integration and Deployment for Vulkan + +Continuous Integration (CI) and Continuous Deployment (CD) are essential practices in modern software development. They help ensure code quality, catch issues early, and streamline the release process. For Vulkan applications, which often need to run on multiple platforms with different GPU architectures, a robust CI/CD pipeline is particularly valuable. + +=== Setting Up a CI/CD Pipeline + +Let's explore how to set up a CI/CD pipeline specifically tailored for Vulkan projects. We'll use GitHub Actions as our example platform, but the concepts apply to other CI/CD systems like GitLab CI, Jenkins, or Azure DevOps. + +==== Basic Pipeline Structure + +A typical CI/CD pipeline for a Vulkan project might include these stages: + +1. *Build*: Compile the application on multiple platforms (Windows, Linux, macOS) +2. *Test*: Run unit tests and integration tests +3. *Package*: Create distributable packages for each platform +4. *Deploy*: Deploy to a staging environment or release to users + +Here's a basic GitHub Actions workflow file for a Vulkan project: + +[source,yaml] +---- +name: Vulkan CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Vulkan SDK + uses: humbletim/install-vulkan-sdk@v1.1.1 + with: + version: latest + cache: true + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} + + - name: Test + working-directory: ${{github.workspace}}/build + run: ctest -C ${{matrix.build_type}} + + - name: Package + if: matrix.build_type == 'Release' + run: | + # Platform-specific packaging commands + if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + # Linux packaging (e.g., .deb or .AppImage) + echo "Packaging for Linux" + elif [ "${{ matrix.os }}" == "windows-latest" ]; then + # Windows packaging (e.g., .exe installer) + echo "Packaging for Windows" + elif [ "${{ matrix.os }}" == "macos-latest" ]; then + # macOS packaging (e.g., .app bundle or .dmg) + echo "Packaging for macOS" + fi +---- + +==== Vulkan-Specific Considerations + +When setting up CI/CD for Vulkan projects, consider these specific challenges: + +===== 1. Vulkan SDK Installation + +Ensure your CI environment has the Vulkan SDK installed. Many CI platforms don't include it by default. In the example above, we used a GitHub Action to install the SDK. + +===== 2. GPU Availability in CI Environments + +Most CI environments don't have GPUs available, which can make testing Vulkan applications challenging. Consider these approaches: + +* Use software rendering (e.g., SwiftShader) for basic tests +* Implement a headless testing mode that doesn't require a display +* Use cloud-based GPU instances for more comprehensive testing + +===== 3. Platform-Specific Vulkan Loaders + +Different platforms handle Vulkan loading differently. Ensure your build system correctly handles these differences: + +* Windows: Vulkan-1.dll is typically loaded at runtime +* Linux: libvulkan.so.1 is loaded at runtime +* macOS: MoltenVK provides Vulkan support via Metal + +===== 4. Shader Compilation + +Shader compilation can be a complex part of the build process. Consider these approaches: + +* Pre-compile shaders during the build phase +* Include shader compilation in your CI pipeline to catch GLSL/SPIR-V errors early +* Use a shader management system that handles cross-platform differences + +=== Automating Testing for Vulkan Applications + +Testing Vulkan applications presents unique challenges. Here are some approaches to consider: + +==== Unit Testing Vulkan Code + +[source,cpp] +---- +import std; +import vulkan_raii; + +// A testable function using vk::raii +bool create_pipeline(vk::raii::Device& device, + vk::raii::RenderPass& render_pass, + vk::raii::PipelineLayout& layout, + vk::raii::Pipeline& out_pipeline) { + try { + // Pipeline creation code using RAII + return true; + } catch (vk::SystemError& err) { + std::cerr << "Failed to create pipeline: " << err.what() << std::endl; + return false; + } +} + +// In a test file +TEST_CASE("Pipeline creation") { + // Setup test environment with mock or real Vulkan objects + vk::raii::Context context; + auto instance = create_test_instance(context); + auto device = create_test_device(instance); + auto render_pass = create_test_render_pass(device); + auto layout = create_test_pipeline_layout(device); + + vk::raii::Pipeline pipeline{nullptr}; + REQUIRE(create_pipeline(device, render_pass, layout, pipeline)); + REQUIRE(pipeline); +} +---- + +==== Integration Testing + +For integration testing, consider creating a headless rendering mode that can run in CI environments: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class HeadlessRenderer { +public: + HeadlessRenderer() { + // Initialize Vulkan without surface + init_vulkan(); + } + + bool render_frame() { + // Render to an image without presenting + try { + // Rendering code + return true; + } catch (vk::SystemError& err) { + std::cerr << "Render failed: " << err.what() << std::endl; + return false; + } + } + + // Compare rendered image with reference + bool verify_output(const std::string& reference_image) { + // Image comparison code + return true; + } + +private: + void init_vulkan() { + // Vulkan initialization code + } + + vk::raii::Context context; + vk::raii::Instance instance{nullptr}; + vk::raii::PhysicalDevice physical_device{nullptr}; + vk::raii::Device device{nullptr}; + // Other Vulkan objects +}; + +// In a test file +TEST_CASE("Render output matches reference") { + HeadlessRenderer renderer; + REQUIRE(renderer.render_frame()); + REQUIRE(renderer.verify_output("reference_image.png")); +} +---- + +=== Distribution Considerations + +Once your application passes all tests, the final stage is packaging and distribution. Here are some considerations: + +==== Packaging Vulkan Applications + +* Include the appropriate Vulkan loader for each platform +* Package shader files or pre-compiled SPIR-V +* Consider using platform-specific packaging tools: + * Windows: NSIS, WiX, or MSIX + * Linux: AppImage, Flatpak, or .deb/.rpm packages + * macOS: DMG or App Store packages + +==== Handling Vulkan Dependencies + +Ensure your package includes or correctly handles all dependencies: + +* Vulkan loader (or instructions to install it) +* Any required Vulkan extensions +* GPU driver requirements + +==== Versioning and Updates + +Implement a versioning system that includes: + +* Application version +* Minimum required Vulkan version +* Required extensions and their versions + +=== Conclusion + +A well-designed CI/CD pipeline is essential for maintaining quality and productivity when developing Vulkan applications. By automating building, testing, and packaging, you can focus more on developing features and less on manual processes. + +In the next section, we'll explore debugging tools for Vulkan applications, including the powerful VK_KHR_debug_utils extension and external tools like RenderDoc. + +link:01_introduction.adoc[Previous: Introduction] | link:03_debugging_and_renderdoc.adoc[Next: Debugging with VK_KHR_debug_utils and RenderDoc] diff --git a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc new file mode 100644 index 00000000..93564766 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc @@ -0,0 +1,369 @@ +::pp: {plus}{plus} + += Tooling: Debugging with VK_KHR_debug_utils and RenderDoc +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Debugging Vulkan Applications + +Debugging graphics applications can be challenging due to their complex, parallel nature and the fact that much of the processing happens on the GPU. Vulkan, with its explicit design, provides powerful debugging tools that can help identify and fix issues in your application. In this section, we'll explore two key approaches to debugging Vulkan applications: + +1. Using the VK_KHR_debug_utils extension for in-application debugging +2. Using external tools like RenderDoc for frame capture and analysis + +=== Using VK_KHR_debug_utils + +The VK_KHR_debug_utils extension provides a comprehensive set of tools for debugging Vulkan applications. It allows you to: + +* Label objects with meaningful names +* Mark the beginning and end of command buffer regions +* Insert debug markers +* Set up debug messengers to receive validation layer messages + +Let's explore how to use these features with C++20 modules and vk::raii. + +==== Setting Up Debug Messaging + +First, let's set up a debug messenger to receive validation layer messages: + +[source,cpp] +---- +import std; +import vulkan_raii; + +// Debug callback function +VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT message_severity, + VkDebugUtilsMessageTypeFlagsEXT message_type, + const VkDebugUtilsMessengerCallbackDataEXT* callback_data, + void* user_data) { + + // Convert severity to string + std::string severity; + if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) { + severity = "VERBOSE"; + } else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) { + severity = "INFO"; + } else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + severity = "WARNING"; + } else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + severity = "ERROR"; + } + + // Convert type to string + std::string type; + if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) { + type = "GENERAL"; + } else if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) { + type = "VALIDATION"; + } else if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) { + type = "PERFORMANCE"; + } + + // Log the message + std::cerr << "[" << severity << ": " << type << "] " + << callback_data->pMessage << std::endl; + + // Return false to indicate the Vulkan call should not be aborted + return VK_FALSE; +} + +// Create a debug messenger using vk::raii +vk::raii::DebugUtilsMessengerEXT create_debug_messenger(vk::raii::Instance& instance) { + vk::DebugUtilsMessengerCreateInfoEXT create_info{}; + create_info.setMessageSeverity( + vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError + ); + create_info.setMessageType( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance + ); + create_info.setPfnUserCallback(debug_callback); + + return vk::raii::DebugUtilsMessengerEXT(instance, create_info); +} +---- + +==== Object Naming + +One of the most useful features of VK_KHR_debug_utils is the ability to give meaningful names to Vulkan objects. This makes debugging much easier, as you can identify objects in validation layer messages and tools like RenderDoc: + +[source,cpp] +---- +// Helper function to set a name on any Vulkan handle +template +void set_object_name(vk::raii::Device& device, T handle, const std::string& name) { + vk::DebugUtilsObjectNameInfoEXT name_info{}; + name_info.setObjectType(get_object_type()); + name_info.setObjectHandle(reinterpret_cast(static_cast(handle))); + name_info.setPObjectName(name.c_str()); + + device.setDebugUtilsObjectNameEXT(name_info); +} + +// Example usage +void name_vulkan_objects(vk::raii::Device& device) { + // Name the device itself + set_object_name(device, *device, "Main Device"); + + // Name a buffer + vk::BufferCreateInfo buffer_info{}; + // ... set buffer creation parameters + vk::raii::Buffer buffer(device, buffer_info); + set_object_name(device, *buffer, "Vertex Buffer"); + + // Name a pipeline + vk::raii::Pipeline pipeline = create_graphics_pipeline(device); + set_object_name(device, *pipeline, "Main Render Pipeline"); +} +---- + +==== Command Buffer Labeling + +You can also label regions of command buffer execution, which helps identify where issues occur during rendering: + +[source,cpp] +---- +void record_command_buffer(vk::raii::CommandBuffer& cmd_buffer) { + cmd_buffer.begin({vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + // Begin a labeled region + vk::DebugUtilsLabelEXT label_info{}; + label_info.setPLabelName("Shadow Pass"); + label_info.setColor(std::array{0.0f, 0.0f, 0.0f, 1.0f}); // Black for shadow pass + cmd_buffer.beginDebugUtilsLabelEXT(label_info); + + // Record shadow pass commands + // ... + + // End the labeled region + cmd_buffer.endDebugUtilsLabelEXT(); + + // Begin another labeled region + label_info.setPLabelName("Main Render Pass"); + label_info.setColor(std::array{0.0f, 1.0f, 0.0f, 1.0f}); // Green for main pass + cmd_buffer.beginDebugUtilsLabelEXT(label_info); + + // Record main render pass commands + // ... + + // Insert a marker within this region + cmd_buffer.insertDebugUtilsLabelEXT({ + "Drawing Opaque Objects", + std::array{1.0f, 1.0f, 1.0f, 1.0f} + }); + + // More rendering commands + // ... + + // End the labeled region + cmd_buffer.endDebugUtilsLabelEXT(); + + cmd_buffer.end(); +} +---- + +==== Queue Labeling + +Similarly, you can label operations submitted to a queue: + +[source,cpp] +---- +void submit_work(vk::raii::Queue& queue, vk::raii::CommandBuffer& cmd_buffer) { + // Begin a labeled region for the queue submission + vk::DebugUtilsLabelEXT label_info{}; + label_info.setPLabelName("Frame Rendering"); + label_info.setColor(std::array{0.0f, 0.5f, 1.0f, 1.0f}); // Blue for frame + queue.beginDebugUtilsLabelEXT(label_info); + + // Submit the command buffer + vk::SubmitInfo submit_info{}; + submit_info.setCommandBufferCount(1); + submit_info.setPCommandBuffers(&(*cmd_buffer)); + queue.submit(submit_info, nullptr); + + // End the labeled region + queue.endDebugUtilsLabelEXT(); +} +---- + +=== Using RenderDoc + +RenderDoc is a powerful graphics debugging tool that allows you to capture frames from your application and analyze them in detail. It's particularly useful for Vulkan applications due to its comprehensive support for the API. + +==== Integrating RenderDoc with Your Application + +You can integrate RenderDoc directly into your application using its in-application API: + +[source,cpp] +---- +import std; +import vulkan_raii; + +#include + +// Load the RenderDoc API +RENDERDOC_API_1_4_1* renderdoc_api = nullptr; + +bool load_renderdoc_api() { + #if defined(_WIN32) + HMODULE renderdoc_module = LoadLibraryA("renderdoc.dll"); + #else + void* renderdoc_module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); + #endif + + if (!renderdoc_module) { + std::cerr << "RenderDoc not loaded in this application" << std::endl; + return false; + } + + #if defined(_WIN32) + pRENDERDOC_GetAPI get_api = (pRENDERDOC_GetAPI)GetProcAddress(renderdoc_module, "RENDERDOC_GetAPI"); + #else + pRENDERDOC_GetAPI get_api = (pRENDERDOC_GetAPI)dlsym(renderdoc_module, "RENDERDOC_GetAPI"); + #endif + + if (!get_api) { + std::cerr << "Failed to get RenderDoc API function" << std::endl; + return false; + } + + int ret = get_api(eRENDERDOC_API_Version_1_4_1, (void**)&renderdoc_api); + if (ret != 1) { + std::cerr << "Failed to initialize RenderDoc API" << std::endl; + return false; + } + + std::cout << "RenderDoc API initialized successfully" << std::endl; + return true; +} + +// Trigger a capture +void capture_frame() { + if (renderdoc_api) { + renderdoc_api->TriggerCapture(); + } +} +---- + +==== Analyzing Captures + +Once you've captured a frame, you can analyze it in the RenderDoc application. Here are some key features to look for: + +1. *Pipeline State*: Examine the full graphics pipeline state for each draw call +2. *Resource Inspection*: View the contents of buffers, textures, and other resources +3. *Shader Debugging*: Step through shader execution for specific pixels +4. *Timing Information*: Analyze performance of different parts of your frame + +==== Best Practices for RenderDoc + +To get the most out of RenderDoc: + +1. *Use Object Names*: As discussed earlier, naming your Vulkan objects makes them much easier to identify in RenderDoc +2. *Use Command Buffer Labels*: These appear in RenderDoc's event browser, making it easier to navigate complex frames +3. *Capture Specific Frames*: Use the in-application API to capture frames where issues occur +4. *Reduce Workload*: For complex applications, consider disabling features or reducing resolution when debugging + +=== Combining VK_KHR_debug_utils and RenderDoc + +The real power comes from combining these approaches: + +1. Use VK_KHR_debug_utils to add rich debugging information to your application +2. Use RenderDoc to capture and analyze frames with this information +3. Use validation layers to catch API usage errors + +Here's an example of setting up a debugging environment that combines these approaches: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class DebugManager { +public: + DebugManager() { + // Try to load RenderDoc API + load_renderdoc_api(); + } + + void setup_instance_debugging(vk::raii::Context& context, vk::InstanceCreateInfo& create_info) { + // Add validation layers + std::vector validation_layers = {"VK_LAYER_KHRONOS_validation"}; + create_info.setPEnabledLayerNames(validation_layers); + + // Add debug utils extension + std::vector extensions = {VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; + // Add any existing extensions + if (create_info.enabledExtensionCount > 0) { + for (uint32_t i = 0; i < create_info.enabledExtensionCount; i++) { + extensions.push_back(create_info.ppEnabledExtensionNames[i]); + } + } + create_info.setPEnabledExtensionNames(extensions); + + // Store debug messenger create info for instance creation + debug_create_info.setMessageSeverity( + vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError + ); + debug_create_info.setMessageType( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance + ); + debug_create_info.setPfnUserCallback(debug_callback); + + // Add to pNext chain + debug_create_info.pNext = create_info.pNext; + create_info.pNext = &debug_create_info; + } + + void setup_debug_messenger(vk::raii::Instance& instance) { + debug_messenger = vk::raii::DebugUtilsMessengerEXT(instance, debug_create_info); + } + + template + void set_name(vk::raii::Device& device, T handle, const std::string& name) { + try { + vk::DebugUtilsObjectNameInfoEXT name_info{}; + name_info.setObjectType(get_object_type()); + name_info.setObjectHandle(reinterpret_cast(static_cast(handle))); + name_info.setPObjectName(name.c_str()); + + device.setDebugUtilsObjectNameEXT(name_info); + } catch (vk::SystemError& err) { + std::cerr << "Failed to set object name: " << err.what() << std::endl; + } + } + + void capture_next_frame() { + if (renderdoc_api) { + renderdoc_api->TriggerCapture(); + } + } + +private: + vk::DebugUtilsMessengerCreateInfoEXT debug_create_info{}; + vk::raii::DebugUtilsMessengerEXT debug_messenger{nullptr}; + RENDERDOC_API_1_4_1* renderdoc_api = nullptr; +}; +---- + +=== Conclusion + +Effective debugging is essential for developing complex Vulkan applications. By combining the power of VK_KHR_debug_utils for in-application debugging and RenderDoc for frame capture and analysis, you can quickly identify and fix issues in your rendering pipeline. + +In the next section, we'll explore crash handling and minidumps, which are crucial for diagnosing issues that occur in production environments. + +link:02_cicd.adoc[Previous: CI/CD for Vulkan Projects] | link:04_crash_minidump.adoc[Next: Crash Handling and Minidumps] diff --git a/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc new file mode 100644 index 00000000..3c8a476c --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc @@ -0,0 +1,499 @@ +::pp: {plus}{plus} + += Tooling: Crash Handling and Minidumps +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Crash Handling in Vulkan Applications + +Even with thorough testing and debugging, crashes can still occur in production environments. When they do, having robust crash handling mechanisms can help you diagnose and fix issues quickly. In this section, we'll explore how to implement crash handling and generate minidumps in Vulkan applications. + +=== Understanding Crashes in Vulkan Applications + +Vulkan applications can crash for various reasons: + +1. *API Usage Errors*: Incorrect use of the Vulkan API that validation layers would catch in debug builds +2. *Driver Bugs*: Issues in the GPU driver that may only manifest with specific hardware or workloads +3. *Resource Management Issues*: Memory leaks, double frees, or accessing destroyed resources +4. *Shader Errors*: Runtime errors in shaders that cause the GPU to hang +5. *System-Level Issues*: Out of memory conditions, system instability, etc. + +Let's explore how to handle these crashes and gather diagnostic information. + +=== Implementing Basic Crash Handling + +First, let's implement a basic crash handler that can catch unhandled exceptions and segmentation faults: + +[source,cpp] +---- +import std; +import vulkan_raii; + +// Global state for crash handling +namespace crash_handler { + std::string app_name; + std::string crash_log_path; + bool initialized = false; + + // Log basic system information + void log_system_info(std::ofstream& log) { + log << "Application: " << app_name << std::endl; + log << "Timestamp: " << std::chrono::system_clock::now() << std::endl; + + // Log OS information + #if defined(_WIN32) + log << "OS: Windows" << std::endl; + #elif defined(__linux__) + log << "OS: Linux" << std::endl; + #elif defined(__APPLE__) + log << "OS: macOS" << std::endl; + #else + log << "OS: Unknown" << std::endl; + #endif + + // Log CPU information + log << "CPU Cores: " << std::thread::hardware_concurrency() << std::endl; + + // Log memory information + #if defined(_WIN32) + MEMORYSTATUSEX mem_info; + mem_info.dwLength = sizeof(MEMORYSTATUSEX); + GlobalMemoryStatusEx(&mem_info); + log << "Total Physical Memory: " << mem_info.ullTotalPhys / (1024 * 1024) << " MB" << std::endl; + log << "Available Memory: " << mem_info.ullAvailPhys / (1024 * 1024) << " MB" << std::endl; + #elif defined(__linux__) + // Linux-specific memory info code + #elif defined(__APPLE__) + // macOS-specific memory info code + #endif + } + + // Log Vulkan-specific information + void log_vulkan_info(std::ofstream& log, vk::raii::PhysicalDevice* physical_device = nullptr) { + if (physical_device) { + auto properties = physical_device->getProperties(); + log << "GPU: " << properties.deviceName << std::endl; + log << "Driver Version: " << properties.driverVersion << std::endl; + log << "Vulkan API Version: " + << VK_VERSION_MAJOR(properties.apiVersion) << "." + << VK_VERSION_MINOR(properties.apiVersion) << "." + << VK_VERSION_PATCH(properties.apiVersion) << std::endl; + } else { + log << "No Vulkan physical device information available" << std::endl; + } + } + + // Handler for unhandled exceptions + void handle_exception(const std::exception& e, vk::raii::PhysicalDevice* physical_device = nullptr) { + try { + std::ofstream log(crash_log_path, std::ios::app); + log << "==== Crash Report ====" << std::endl; + log_system_info(log); + log_vulkan_info(log, physical_device); + + log << "Exception: " << e.what() << std::endl; + log << "==== End of Crash Report ====" << std::endl << std::endl; + + log.close(); + } catch (...) { + // Last resort if we can't even write to the log + std::cerr << "Failed to write crash log" << std::endl; + } + } + + // Signal handler for segfaults, etc. + void signal_handler(int signal) { + try { + std::ofstream log(crash_log_path, std::ios::app); + log << "==== Crash Report ====" << std::endl; + log_system_info(log); + + log << "Signal: " << signal << " ("; + switch (signal) { + case SIGSEGV: log << "SIGSEGV - Segmentation fault"; break; + case SIGILL: log << "SIGILL - Illegal instruction"; break; + case SIGFPE: log << "SIGFPE - Floating point exception"; break; + case SIGABRT: log << "SIGABRT - Abort"; break; + default: log << "Unknown signal"; break; + } + log << ")" << std::endl; + + log << "==== End of Crash Report ====" << std::endl << std::endl; + + log.close(); + } catch (...) { + // Last resort if we can't even write to the log + std::cerr << "Failed to write crash log" << std::endl; + } + + // Re-raise the signal for the default handler + signal(signal, SIG_DFL); + raise(signal); + } + + // Initialize the crash handler + void initialize(const std::string& application_name, const std::string& log_path) { + if (initialized) return; + + app_name = application_name; + crash_log_path = log_path; + + // Set up signal handlers + signal(SIGSEGV, signal_handler); + signal(SIGILL, signal_handler); + signal(SIGFPE, signal_handler); + signal(SIGABRT, signal_handler); + + initialized = true; + } +} + +// Example usage in main application +int main() { + try { + // Initialize crash handler + crash_handler::initialize("MyVulkanApp", "crash_log.txt"); + + // Initialize Vulkan + vk::raii::Context context; + auto instance = create_instance(context); + auto physical_device = select_physical_device(instance); + auto device = create_device(physical_device); + + // Main application loop + while (true) { + try { + // Render frame + render_frame(device); + } catch (const vk::SystemError& e) { + // Handle Vulkan errors that we can recover from + std::cerr << "Vulkan error: " << e.what() << std::endl; + } + } + } catch (const std::exception& e) { + // Handle unrecoverable exceptions + crash_handler::handle_exception(e); + return 1; + } + + return 0; +} +---- + +=== Generating Minidumps + +While basic crash logs are helpful, minidumps provide much more detailed information for diagnosing crashes. A minidump is a file containing a snapshot of the process memory and state at the time of the crash. + +Let's implement minidump generation using platform-specific APIs: + +[source,cpp] +---- +import std; +import vulkan_raii; + +namespace crash_handler { + std::string app_name; + std::string dump_path; + bool initialized = false; + + #if defined(_WIN32) + // Windows implementation using Windows Error Reporting (WER) + LONG WINAPI windows_exception_handler(EXCEPTION_POINTERS* exception_pointers) { + // Create a unique filename for the minidump + std::string filename = dump_path + "\\" + app_name + "_" + + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ".dmp"; + + // Create the minidump file + HANDLE file = CreateFileA( + filename.c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + ); + + if (file != INVALID_HANDLE_VALUE) { + // Initialize minidump info + MINIDUMP_EXCEPTION_INFORMATION exception_info; + exception_info.ThreadId = GetCurrentThreadId(); + exception_info.ExceptionPointers = exception_pointers; + exception_info.ClientPointers = FALSE; + + // Write the minidump + MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + file, + MiniDumpWithFullMemory, // Dump type + &exception_info, + nullptr, + nullptr + ); + + CloseHandle(file); + + std::cerr << "Minidump written to: " << filename << std::endl; + } else { + std::cerr << "Failed to create minidump file" << std::endl; + } + + // Continue with normal exception handling + return EXCEPTION_CONTINUE_SEARCH; + } + + void initialize(const std::string& application_name, const std::string& minidump_path) { + if (initialized) return; + + app_name = application_name; + dump_path = minidump_path; + + // Create the dump directory if it doesn't exist + CreateDirectoryA(dump_path.c_str(), nullptr); + + // Set up the exception handler + SetUnhandledExceptionFilter(windows_exception_handler); + + initialized = true; + } + + #elif defined(__linux__) + // Linux implementation using Google Breakpad + // Note: This requires linking against the Google Breakpad library + + #include "client/linux/handler/exception_handler.h" + + // Callback for when a minidump is generated + static bool minidump_callback(const google_breakpad::MinidumpDescriptor& descriptor, + void* context, bool succeeded) { + std::cerr << "Minidump generated: " << descriptor.path() << std::endl; + return succeeded; + } + + google_breakpad::ExceptionHandler* exception_handler = nullptr; + + void initialize(const std::string& application_name, const std::string& minidump_path) { + if (initialized) return; + + app_name = application_name; + dump_path = minidump_path; + + // Create the dump directory if it doesn't exist + std::filesystem::create_directories(dump_path); + + // Set up the exception handler + google_breakpad::MinidumpDescriptor descriptor(dump_path); + exception_handler = new google_breakpad::ExceptionHandler( + descriptor, + nullptr, + minidump_callback, + nullptr, + true, + -1 + ); + + initialized = true; + } + + #elif defined(__APPLE__) + // macOS implementation using Google Breakpad + // Similar to Linux implementation + #endif +} +---- + +=== Analyzing Minidumps + +Once you have a minidump, you need to analyze it to determine the cause of the crash. Here's how to do this on different platforms: + +==== Windows + +On Windows, you can use Visual Studio or WinDbg to analyze minidumps: + +1. *Visual Studio*: + - Open Visual Studio + - Go to File > Open > File and select the .dmp file + - Visual Studio will load the minidump and show the call stack at the time of the crash + +2. *WinDbg*: + - Open WinDbg + - Open the minidump file + - Use commands like `.ecxr` to examine the exception context record + - Use `k` to view the call stack + +==== Linux and macOS + +On Linux and macOS, you can use tools like GDB or LLDB to analyze minidumps generated by Google Breakpad: + +1. *Using minidump_stackwalk* (part of Google Breakpad): + ``` + minidump_stackwalk minidump_file.dmp /path/to/symbols > stacktrace.txt + ``` + +2. *Using GDB*: + ``` + gdb /path/to/executable + (gdb) core-file /path/to/minidump + (gdb) bt + ``` + +=== Vulkan-Specific Crash Information + +For Vulkan applications, it's helpful to include additional information in your crash reports: + +[source,cpp] +---- +void log_vulkan_detailed_info(std::ofstream& log, vk::raii::PhysicalDevice& physical_device, + vk::raii::Device& device) { + // Log physical device properties + auto properties = physical_device.getProperties(); + log << "GPU: " << properties.deviceName << std::endl; + log << "Driver Version: " << properties.driverVersion << std::endl; + log << "Vulkan API Version: " + << VK_VERSION_MAJOR(properties.apiVersion) << "." + << VK_VERSION_MINOR(properties.apiVersion) << "." + << VK_VERSION_PATCH(properties.apiVersion) << std::endl; + + // Log memory usage + auto memory_properties = physical_device.getMemoryProperties(); + log << "Memory Heaps:" << std::endl; + for (uint32_t i = 0; i < memory_properties.memoryHeapCount; i++) { + log << " Heap " << i << ": " + << (memory_properties.memoryHeaps[i].size / (1024 * 1024)) << " MB"; + if (memory_properties.memoryHeaps[i].flags & vk::MemoryHeapFlagBits::eDeviceLocal) { + log << " (Device Local)"; + } + log << std::endl; + } + + // Log enabled extensions + auto extensions = device.enumerateDeviceExtensionProperties(); + log << "Enabled Extensions:" << std::endl; + for (const auto& ext : extensions) { + log << " " << ext.extensionName << " (version " << ext.specVersion << ")" << std::endl; + } + + // Log current pipeline cache state + // This can be useful for diagnosing shader-related crashes + try { + auto pipeline_cache_data = device.getPipelineCacheData(); + log << "Pipeline Cache Size: " << pipeline_cache_data.size() << " bytes" << std::endl; + } catch (const vk::SystemError& e) { + log << "Failed to get pipeline cache data: " << e.what() << std::endl; + } +} +---- + +=== Integrating with Telemetry Systems + +For production applications, you might want to automatically upload crash reports to a telemetry system for analysis: + +[source,cpp] +---- +import std; +import vulkan_raii; +#include + +namespace crash_handler { + // ... existing code ... + + std::string telemetry_url; + bool telemetry_enabled = false; + + // Upload a minidump to the telemetry server + bool upload_minidump(const std::string& minidump_path) { + if (!telemetry_enabled || telemetry_url.empty()) { + return false; + } + + CURL* curl = curl_easy_init(); + if (!curl) { + std::cerr << "Failed to initialize curl" << std::endl; + return false; + } + + // Set up the form data + curl_mime* form = curl_mime_init(curl); + + // Add the minidump file + curl_mimepart* field = curl_mime_addpart(form); + curl_mime_name(field, "minidump"); + curl_mime_filedata(field, minidump_path.c_str()); + + // Add application information + field = curl_mime_addpart(form); + curl_mime_name(field, "product"); + curl_mime_data(field, app_name.c_str(), CURL_ZERO_TERMINATED); + + // Add version information + field = curl_mime_addpart(form); + curl_mime_name(field, "version"); + curl_mime_data(field, "1.0.0", CURL_ZERO_TERMINATED); // Replace with your version + + // Set up the request + curl_easy_setopt(curl, CURLOPT_URL, telemetry_url.c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, form); + + // Perform the request + CURLcode res = curl_easy_perform(curl); + + // Clean up + curl_mime_free(form); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + std::cerr << "Failed to upload minidump: " << curl_easy_strerror(res) << std::endl; + return false; + } + + return true; + } + + // Enable telemetry + void enable_telemetry(const std::string& url) { + telemetry_url = url; + telemetry_enabled = true; + + // Initialize curl + curl_global_init(CURL_GLOBAL_ALL); + } + + // Disable telemetry + void disable_telemetry() { + telemetry_enabled = false; + + // Clean up curl + curl_global_cleanup(); + } +} +---- + +=== Best Practices for Crash Handling + +To make the most of your crash handling system: + +1. *Always Include Version Information*: Make sure your crash reports include the application version, Vulkan version, and driver version. + +2. *Collect Relevant State*: Include information about what the application was doing when it crashed (e.g., loading a model, rendering a specific scene). + +3. *Respect User Privacy*: Be transparent about what data you collect and get user consent before uploading crash reports. + +4. *Test Your Crash Handling*: Deliberately trigger crashes in different scenarios to ensure your crash handling system works correctly. + +5. *Implement Graceful Recovery*: When possible, try to recover from non-fatal errors rather than crashing. + +6. *Use Crash Reports to Improve*: Regularly analyze crash reports to identify and fix common issues. + +=== Conclusion + +Robust crash handling is essential for maintaining a high-quality Vulkan application. By implementing proper crash handling and minidump generation, you can quickly diagnose and fix issues that occur in production environments, leading to a more stable and reliable application. + +In the next section, we'll explore Vulkan extensions for robustness, which can help prevent crashes in the first place by making your application more resilient to undefined behavior. + +link:03_debugging_and_renderdoc.adoc[Previous: Debugging with VK_KHR_debug_utils and RenderDoc] | link:05_extensions.adoc[Next: Vulkan Extensions for Robustness] diff --git a/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc new file mode 100644 index 00000000..0ec71b3c --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc @@ -0,0 +1,314 @@ +::pp: {plus}{plus} + += Tooling: Vulkan Extensions for Robustness +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Vulkan Extensions for Robustness + +Vulkan's explicit design gives developers fine-grained control over the graphics pipeline, but this control comes with responsibility. Undefined behavior can occur when applications make mistakes like accessing out-of-bounds memory or using uninitialized resources. In this section, we'll explore Vulkan extensions that can help make your application more robust against such issues, with a particular focus on VK_EXT_robustness2. + +=== Understanding Undefined Behavior in Vulkan + +Before diving into robustness extensions, let's understand what kinds of undefined behavior can occur in Vulkan applications: + +1. *Out-of-bounds Access*: Accessing memory outside the bounds of a buffer or image +2. *Use-after-free*: Using a resource after it has been destroyed +3. *Uninitialized Memory*: Reading from memory that hasn't been initialized +4. *Invalid Descriptors*: Using descriptors that point to invalid or incompatible resources +5. *Shader Execution Errors*: Division by zero, infinite loops, etc. + +In standard Vulkan, these errors can lead to unpredictable behavior, including: + +* Application crashes +* GPU hangs requiring a system restart +* Corrupted rendering +* Security vulnerabilities +* Inconsistent behavior across different hardware + +Robustness extensions aim to provide more predictable behavior in these scenarios, often at a small performance cost. + +=== VK_EXT_robustness2 Extension + +The VK_EXT_robustness2 extension is an improved version of the original VK_EXT_robustness extension. It provides more comprehensive protection against undefined behavior, particularly for out-of-bounds accesses. + +==== Key Features + +VK_EXT_robustness2 offers several important features: + +1. *Robust Buffer Access*: Out-of-bounds reads from buffers return zero values instead of causing undefined behavior +2. *Robust Image Access*: Out-of-bounds reads from images return zero or transparent black +3. *Null Descriptor Handling*: Reads from null descriptors return zero values +4. *Robust Buffer Access 2*: An improved version that also handles out-of-bounds writes by discarding them + +==== Enabling VK_EXT_robustness2 + +Let's see how to enable and use this extension. + +[source,cpp] +---- +bool check_robustness2_support(vk::raii::PhysicalDevice& physical_device) { + // Check if the extension is supported + auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); + + for (const auto& extension : available_extensions) { + if (strcmp(extension.extensionName, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) == 0) { + return true; + } + } + + return false; +} + +void enable_robustness2(vk::DeviceCreateInfo& device_create_info, + std::vector& enabled_extensions) { + // Add the extension to the list of enabled extensions + enabled_extensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); + device_create_info.setPEnabledExtensionNames(enabled_extensions); + + // Set up the robustness2 features + vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features{}; + robustness2_features.setRobustBufferAccess2(VK_TRUE); + robustness2_features.setRobustImageAccess2(VK_TRUE); + robustness2_features.setNullDescriptor(VK_TRUE); + + // Add to the pNext chain + robustness2_features.pNext = device_create_info.pNext; + device_create_info.pNext = &robustness2_features; +} + +vk::raii::Device create_robust_device(vk::raii::PhysicalDevice& physical_device, + vk::raii::Instance& instance) { + // Check for support + if (!check_robustness2_support(physical_device)) { + std::cerr << "VK_EXT_robustness2 is not supported on this device" << std::endl; + // Fall back to less robust behavior or abort + } + + // Set up device creation + std::vector enabled_extensions; + // Add your other required extensions here + + vk::DeviceCreateInfo create_info{}; + // Set up your queues, features, etc. + + // Enable robustness2 + enable_robustness2(create_info, enabled_extensions); + + // Create the device + return vk::raii::Device(physical_device, create_info); +} +---- + +==== Using Robust Access in Practice + +Once you've enabled the extension, robust buffer and image access will be applied automatically. However, you should be aware of some considerations: + +1. *Performance Impact*: Robust access can have a performance cost, as the GPU needs to perform bounds checking +2. *Not a Substitute for Correctness*: While robustness extensions make your application more resilient, they don't fix the underlying bugs +3. *Debug vs. Release*: Consider enabling robustness in debug builds for development and testing, but evaluate the performance impact for release builds + +Here's an example of how robust buffer access can prevent crashes: + +[source,cpp] +---- +// Without robust buffer access, this could crash or produce undefined results +void potentially_dangerous_operation(vk::raii::CommandBuffer& cmd_buffer, + vk::raii::Buffer& buffer, + vk::raii::DescriptorSet& descriptor_set, + uint32_t dynamic_offset, + uint32_t buffer_size) { + // If dynamic_offset is too large, this would normally cause undefined behavior + // With robust buffer access, out-of-bounds reads will return zero + cmd_buffer.bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + pipeline_layout, + 0, + 1, + &(*descriptor_set), + 1, + &dynamic_offset + ); + + // Dispatch compute work that might read out of bounds + cmd_buffer.dispatch(buffer_size / 64 + 1, 1, 1); // Potentially too many workgroups +} +---- + +=== Other Robustness Extensions + +While VK_EXT_robustness2 is the focus of this section, there are other extensions that can help improve application robustness: + +==== VK_KHR_buffer_device_address + +This extension allows you to use physical device addresses for buffers, which can be useful for advanced techniques. It includes robustness features for handling invalid addresses: + +[source,cpp] +---- +void enable_buffer_device_address(vk::DeviceCreateInfo& device_create_info, + std::vector& enabled_extensions) { + enabled_extensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + device_create_info.setPEnabledExtensionNames(enabled_extensions); + + vk::PhysicalDeviceBufferDeviceAddressFeatures buffer_device_address_features{}; + buffer_device_address_features.setBufferDeviceAddress(VK_TRUE); + buffer_device_address_features.setBufferDeviceAddressCaptureReplay(VK_TRUE); + + // Add to the pNext chain + buffer_device_address_features.pNext = device_create_info.pNext; + device_create_info.pNext = &buffer_device_address_features; +} +---- + +==== VK_EXT_descriptor_indexing + +This extension allows for more flexible descriptor indexing, including robustness features for handling out-of-bounds descriptor array accesses: + +[source,cpp] +---- +void enable_descriptor_indexing(vk::DeviceCreateInfo& device_create_info, + std::vector& enabled_extensions) { + enabled_extensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + device_create_info.setPEnabledExtensionNames(enabled_extensions); + + vk::PhysicalDeviceDescriptorIndexingFeatures indexing_features{}; + indexing_features.setRuntimeDescriptorArray(VK_TRUE); + indexing_features.setDescriptorBindingPartiallyBound(VK_TRUE); + indexing_features.setDescriptorBindingSampledImageUpdateAfterBind(VK_TRUE); + indexing_features.setDescriptorBindingStorageBufferUpdateAfterBind(VK_TRUE); + + // Add to the pNext chain + indexing_features.pNext = device_create_info.pNext; + device_create_info.pNext = &indexing_features; +} +---- + +=== Combining Robustness Extensions with Debugging Tools + +For maximum effectiveness, combine robustness extensions with the debugging tools we discussed in previous sections: + +[source,cpp] +---- +class RobustVulkanApplication { +public: + RobustVulkanApplication() { + initialize_vulkan(); + } + + void run() { + // Main application loop + while (!should_close()) { + try { + update(); + render(); + } catch (const vk::SystemError& e) { + // Handle recoverable Vulkan errors + std::cerr << "Vulkan error: " << e.what() << std::endl; + // Attempt recovery + if (!recover_from_error()) { + break; + } + } + } + + cleanup(); + } + +private: + void initialize_vulkan() { + // Create instance with validation layers in debug builds + #ifdef _DEBUG + enable_validation_layers = true; + #else + enable_validation_layers = false; + #endif + + instance = create_instance(); + + // Set up debug messenger if validation is enabled + if (enable_validation_layers) { + debug_messenger = create_debug_messenger(instance); + } + + // Select physical device + physical_device = select_physical_device(instance); + + // Check for robustness support + has_robustness2 = check_robustness2_support(physical_device); + + // Create logical device with robustness if available + device = create_device(physical_device); + + // Initialize other Vulkan resources + // ... + } + + vk::raii::Device create_device(vk::raii::PhysicalDevice& physical_device) { + std::vector extensions; + // Add required extensions + + vk::DeviceCreateInfo create_info{}; + // Set up queues, etc. + + // Enable robustness if available + if (has_robustness2) { + enable_robustness2(create_info, extensions); + } + + // Enable other robustness-related extensions + enable_buffer_device_address(create_info, extensions); + enable_descriptor_indexing(create_info, extensions); + + return vk::raii::Device(physical_device, create_info); + } + + bool recover_from_error() { + // Attempt to recover from errors + // This might involve recreating swapchain, command buffers, etc. + try { + // Reset command buffers + // Recreate swapchain if needed + // ... + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to recover: " << e.what() << std::endl; + return false; + } + } + + // Vulkan objects + vk::raii::Context context; + vk::raii::Instance instance{nullptr}; + vk::raii::DebugUtilsMessengerEXT debug_messenger{nullptr}; + vk::raii::PhysicalDevice physical_device{nullptr}; + vk::raii::Device device{nullptr}; + + // Flags + bool enable_validation_layers = false; + bool has_robustness2 = false; +}; +---- + +=== Best Practices for Using Robustness Extensions + +To make the most of robustness extensions: + +1. *Check for Support*: Always check if the extension is supported before trying to use it +2. *Fallback Behavior*: Implement fallback behavior for devices that don't support the extensions +3. *Performance Testing*: Measure the performance impact of enabling robustness features +4. *Combine with Validation*: Use validation layers during development to catch issues early +5. *Don't Rely on Robustness*: Fix the underlying issues rather than relying on robustness extensions to mask them +6. *Document Usage*: Clearly document which extensions your application requires and why + +=== Conclusion + +Vulkan robustness extensions, particularly VK_EXT_robustness2, provide valuable tools for making your application more resilient to undefined behavior. By combining these extensions with proper error handling, validation layers, and debugging tools, you can create a more stable and reliable Vulkan application. + +In the next and final section, we'll summarize what we've learned about tooling for Vulkan applications and discuss how to apply these techniques in your own projects. + +link:04_crash_minidump.adoc[Previous: Crash Handling and Minidumps] | link:06_packaging_and_distribution.adoc[Next: Packaging and Distribution] diff --git a/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc b/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc new file mode 100644 index 00000000..426fd0b8 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc @@ -0,0 +1,548 @@ +::pp: {plus}{plus} + += Tooling: Packaging and Distribution +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Packaging and Distributing Vulkan Applications + +After developing and testing your Vulkan application, the final step is to package and distribute it to users. This process involves preparing your application for different platforms, handling dependencies, and creating installers or packages that provide a smooth installation experience. In this section, we'll explore the key considerations and techniques for packaging and distributing Vulkan applications. + +=== Platform-Specific Packaging Considerations + +Each platform has its own packaging formats and distribution mechanisms. Let's explore the considerations for the major platforms: + +==== Windows Packaging + +On Windows, common packaging formats include: + +* *Executable Installers*: Created with tools like NSIS (Nullsoft Scriptable Install System), Inno Setup, or WiX Toolset +* *MSIX Packages*: Modern Windows app packages that support clean installation and uninstallation +* *Portable Applications*: Self-contained applications that don't require installation + +Here's an example of creating a basic NSIS installer script for a Vulkan application: + +[source,nsis] +---- +; Basic NSIS installer script for a Vulkan application + +!include "MUI2.nsh" + +Name "My Vulkan Application" +OutFile "MyVulkanApp_Installer.exe" +InstallDir "$PROGRAMFILES\MyVulkanApp" + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +Section "Install" + SetOutPath "$INSTDIR" + + ; Application files + File "MyVulkanApp.exe" + File "*.dll" + File /r "shaders" + File /r "assets" + + ; Vulkan Runtime + File "vulkan-1.dll" + + ; Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ; Create shortcuts + CreateDirectory "$SMPROGRAMS\MyVulkanApp" + CreateShortcut "$SMPROGRAMS\MyVulkanApp\MyVulkanApp.lnk" "$INSTDIR\MyVulkanApp.exe" + CreateShortcut "$SMPROGRAMS\MyVulkanApp\Uninstall.lnk" "$INSTDIR\Uninstall.exe" +SectionEnd + +Section "Uninstall" + ; Remove application files + Delete "$INSTDIR\MyVulkanApp.exe" + Delete "$INSTDIR\*.dll" + RMDir /r "$INSTDIR\shaders" + RMDir /r "$INSTDIR\assets" + + ; Remove uninstaller + Delete "$INSTDIR\Uninstall.exe" + + ; Remove shortcuts + Delete "$SMPROGRAMS\MyVulkanApp\MyVulkanApp.lnk" + Delete "$SMPROGRAMS\MyVulkanApp\Uninstall.lnk" + RMDir "$SMPROGRAMS\MyVulkanApp" + + ; Remove install directory + RMDir "$INSTDIR" +SectionEnd +---- + +==== Linux Packaging + +On Linux, common packaging formats include: + +* *DEB Packages*: For Debian-based distributions (Ubuntu, Debian, etc.) +* *RPM Packages*: For Red Hat-based distributions (Fedora, CentOS, etc.) +* *AppImage*: Self-contained applications that run on most Linux distributions +* *Flatpak*: Sandboxed applications with controlled access to system resources +* *Snap*: Universal Linux packages maintained by Canonical + +Here's an example of creating a basic AppImage for a Vulkan application: + +[source,bash] +---- +#!/bin/bash +# Script to create an AppImage for a Vulkan application + +# Create AppDir structure +mkdir -p AppDir/usr/bin +mkdir -p AppDir/usr/lib +mkdir -p AppDir/usr/share/applications +mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps +mkdir -p AppDir/usr/share/metainfo + +# Copy application binary +cp build/MyVulkanApp AppDir/usr/bin/ + +# Copy dependencies (excluding system libraries) +ldd build/MyVulkanApp | grep "=> /" | awk '{print $3}' | xargs -I{} cp -v {} AppDir/usr/lib/ + +# Copy Vulkan loader +cp /usr/lib/libvulkan.so.1 AppDir/usr/lib/ + +# Copy application data +cp -r assets AppDir/usr/share/MyVulkanApp/assets +cp -r shaders AppDir/usr/share/MyVulkanApp/shaders + +# Create desktop file +cat > AppDir/usr/share/applications/MyVulkanApp.desktop << EOF +[Desktop Entry] +Name=My Vulkan Application +Exec=MyVulkanApp +Icon=MyVulkanApp +Type=Application +Categories=Graphics; +EOF + +# Copy icon +cp icon.png AppDir/usr/share/icons/hicolor/256x256/apps/MyVulkanApp.png + +# Create AppStream metadata +cat > AppDir/usr/share/metainfo/MyVulkanApp.appdata.xml << EOF + + + com.example.MyVulkanApp + My Vulkan Application + A Vulkan-powered application + +

+ My Vulkan Application is a high-performance graphics application + built with the Vulkan API. +

+
+ https://example.com/MyVulkanApp + + + +
+EOF + +# Create AppRun script +cat > AppDir/AppRun << EOF +#!/bin/bash +SELF=\$(readlink -f "\$0") +HERE=\$(dirname "\$SELF") +export PATH="\${HERE}/usr/bin:\${PATH}" +export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}" +export VK_LAYER_PATH="\${HERE}/usr/share/vulkan/explicit_layer.d" +export VK_ICD_FILENAMES="\${HERE}/usr/share/vulkan/icd.d/vulkan_icd.json" +"\${HERE}/usr/bin/MyVulkanApp" "$@" +EOF + +chmod +x AppDir/AppRun + +# Download appimagetool +wget -c "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" +chmod +x appimagetool-x86_64.AppImage + +# Create the AppImage +./appimagetool-x86_64.AppImage AppDir MyVulkanApp-x86_64.AppImage +---- + +==== macOS Packaging + +On macOS, common packaging formats include: + +* *Application Bundles (.app)*: The standard format for macOS applications +* *Disk Images (.dmg)*: Mountable disk images containing the application +* *Packages (.pkg)*: Installer packages for more complex installations + +Here's an example of creating a basic macOS application bundle structure for a Vulkan application using MoltenVK: + +[source,bash] +---- +#!/bin/bash +# Script to create a macOS application bundle for a Vulkan application + +# Create bundle structure +mkdir -p MyVulkanApp.app/Contents/MacOS +mkdir -p MyVulkanApp.app/Contents/Resources +mkdir -p MyVulkanApp.app/Contents/Frameworks + +# Copy application binary +cp build/MyVulkanApp MyVulkanApp.app/Contents/MacOS/ + +# Copy MoltenVK framework +cp -R $VULKAN_SDK/macOS/Frameworks/MoltenVK.framework MyVulkanApp.app/Contents/Frameworks/ + +# Copy application resources +cp -r assets MyVulkanApp.app/Contents/Resources/assets +cp -r shaders MyVulkanApp.app/Contents/Resources/shaders +cp icon.icns MyVulkanApp.app/Contents/Resources/ + +# Create Info.plist +cat > MyVulkanApp.app/Contents/Info.plist << EOF + + + + + CFBundleExecutable + MyVulkanApp + CFBundleIconFile + icon.icns + CFBundleIdentifier + com.example.MyVulkanApp + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + My Vulkan Application + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1 + NSHighResolutionCapable + + + +EOF + +# Create DMG (optional) +hdiutil create -volname "My Vulkan Application" -srcfolder MyVulkanApp.app -ov -format UDZO MyVulkanApp.dmg +---- + +=== Handling Vulkan Dependencies + +One of the key considerations when packaging Vulkan applications is handling the Vulkan loader and any required extensions. + +==== Vulkan Loader + +The Vulkan loader is the component that connects your application to the Vulkan implementation on the user's system. There are different approaches to handling the loader: + +1. *Rely on System-Installed Loader*: Require users to have the Vulkan SDK or drivers installed +2. *Bundle the Loader*: Include the Vulkan loader with your application +3. *Hybrid Approach*: Check for a system-installed loader and fall back to a bundled one if not found + +Here's an example of a hybrid approach: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class VulkanLoader { +public: + static bool initialize() { + try { + // First, try to use the system-installed Vulkan loader + if (try_system_loader()) { + std::cout << "Using system-installed Vulkan loader" << std::endl; + return true; + } + + // If that fails, try to use the bundled loader + if (try_bundled_loader()) { + std::cout << "Using bundled Vulkan loader" << std::endl; + return true; + } + + // If both approaches fail, report an error + std::cerr << "Failed to initialize Vulkan loader" << std::endl; + return false; + } catch (const std::exception& e) { + std::cerr << "Error initializing Vulkan loader: " << e.what() << std::endl; + return false; + } + } + +private: + static bool try_system_loader() { + try { + // Create a Vulkan instance to test if the system loader works + vk::raii::Context context; + vk::ApplicationInfo app_info{}; + app_info.setApiVersion(VK_API_VERSION_1_2); + + vk::InstanceCreateInfo create_info{}; + create_info.setPApplicationInfo(&app_info); + + vk::raii::Instance instance(context, create_info); + return true; + } catch (...) { + return false; + } + } + + static bool try_bundled_loader() { + try { + // Set the path to the bundled Vulkan loader + #if defined(_WIN32) + std::string loader_path = get_executable_path() + "\\vulkan-1.dll"; + SetDllDirectoryA(get_executable_path().c_str()); + #elif defined(__linux__) + std::string loader_path = get_executable_path() + "/libvulkan.so.1"; + setenv("LD_LIBRARY_PATH", get_executable_path().c_str(), 1); + #elif defined(__APPLE__) + std::string loader_path = get_executable_path() + "/../Frameworks/libMoltenVK.dylib"; + setenv("DYLD_LIBRARY_PATH", (get_executable_path() + "/../Frameworks").c_str(), 1); + #endif + + // Check if the bundled loader exists + if (!std::filesystem::exists(loader_path)) { + return false; + } + + // Try to create a Vulkan instance using the bundled loader + vk::raii::Context context; + vk::ApplicationInfo app_info{}; + app_info.setApiVersion(VK_API_VERSION_1_2); + + vk::InstanceCreateInfo create_info{}; + create_info.setPApplicationInfo(&app_info); + + vk::raii::Instance instance(context, create_info); + return true; + } catch (...) { + return false; + } + } + + static std::string get_executable_path() { + #if defined(_WIN32) + char path[MAX_PATH]; + GetModuleFileNameA(NULL, path, MAX_PATH); + std::string exe_path(path); + return exe_path.substr(0, exe_path.find_last_of("\\/")); + #elif defined(__linux__) + char result[PATH_MAX]; + ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); + std::string exe_path(result, (count > 0) ? count : 0); + return exe_path.substr(0, exe_path.find_last_of("/")); + #elif defined(__APPLE__) + char path[PATH_MAX]; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + std::string exe_path(path); + return exe_path.substr(0, exe_path.find_last_of("/")); + } + return ""; + #endif + } +}; +---- + +==== Vulkan Layers and Extensions + +If your application requires specific Vulkan layers or extensions, you need to handle them appropriately: + +1. *Document Requirements*: Clearly document which extensions your application requires +2. *Check for Support*: Always check if required extensions are available before using them +3. *Provide Fallbacks*: Implement fallback behavior for missing extensions when possible +4. *Bundle Layers*: For development tools, consider bundling validation layers + +=== Shader Management + +Shaders are a critical part of Vulkan applications, and they need special consideration during packaging: + +1. *Pre-Compile Shaders*: Package pre-compiled SPIR-V shaders rather than GLSL source +2. *Shader Versioning*: Implement a versioning system for shaders to handle updates +3. *Shader Optimization*: Consider optimizing shaders for different hardware targets +4. *Shader Caching*: Implement a shader cache to improve load times + +Here's an example of a shader management system for a packaged application: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class ShaderManager { +public: + ShaderManager(vk::raii::Device& device) : device(device) { + // Determine the shader directory based on the application's location + shader_dir = get_application_directory() + "/shaders"; + + // Create a shader module cache + shader_cache.reserve(100); // Reserve space for up to 100 shader modules + } + + vk::raii::ShaderModule load_shader(const std::string& name) { + // Check if the shader is already in the cache + auto it = shader_cache.find(name); + if (it != shader_cache.end()) { + return vk::raii::ShaderModule(nullptr, nullptr, nullptr); // Return a copy of the cached module + } + + // Load the shader from the package + std::string path = shader_dir + "/" + name + ".spv"; + std::vector code = read_file(path); + + // Create the shader module + vk::ShaderModuleCreateInfo create_info{}; + create_info.setCodeSize(code.size()); + create_info.setPCode(reinterpret_cast(code.data())); + + // Create and cache the shader module + vk::raii::ShaderModule module(device, create_info); + shader_cache[name] = std::move(module); + + return vk::raii::ShaderModule(nullptr, nullptr, nullptr); // Return a copy of the cached module + } + + void clear_cache() { + shader_cache.clear(); + } + +private: + std::string get_application_directory() { + // Platform-specific code to get the application directory + // ... + return "."; // Placeholder + } + + std::vector read_file(const std::string& path) { + std::ifstream file(path, std::ios::ate | std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open shader file: " + path); + } + + size_t file_size = static_cast(file.tellg()); + std::vector buffer(file_size); + + file.seekg(0); + file.read(buffer.data(), file_size); + file.close(); + + return buffer; + } + + vk::raii::Device& device; + std::string shader_dir; + std::unordered_map shader_cache; +}; +---- + +=== Automated Packaging with CI/CD + +As we discussed in the CI/CD section, automating the packaging process can save time and reduce errors. Here's how to integrate packaging into your CI/CD pipeline: + +1. *Build Matrix*: Set up a build matrix for different platforms and configurations +2. *Packaging Scripts*: Create scripts for each platform's packaging process +3. *Version Management*: Automatically increment version numbers based on git tags or other criteria +4. *Artifact Storage*: Store packaged applications as build artifacts +5. *Release Automation*: Automate the release process to distribution platforms + +Here's an example of a GitHub Actions workflow that includes packaging: + +[source,yaml] +---- +name: Build and Package + +on: + push: + tags: + - 'v*' + +jobs: + build-and-package: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + package-script: ./scripts/package_linux.sh + artifact-name: MyVulkanApp-Linux + artifact-path: MyVulkanApp-x86_64.AppImage + - os: windows-latest + package-script: .\scripts\package_windows.bat + artifact-name: MyVulkanApp-Windows + artifact-path: MyVulkanApp_Installer.exe + - os: macos-latest + package-script: ./scripts/package_macos.sh + artifact-name: MyVulkanApp-macOS + artifact-path: MyVulkanApp.dmg + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Vulkan SDK + uses: humbletim/install-vulkan-sdk@v1.1.1 + with: + version: latest + cache: true + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build ${{github.workspace}}/build --config Release + + - name: Package + run: ${{ matrix.package-script }} + + - name: Upload Package + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.artifact-name }} + path: ${{ matrix.artifact-path }} + + create-release: + needs: build-and-package + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v3 + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + MyVulkanApp-Linux/MyVulkanApp-x86_64.AppImage + MyVulkanApp-Windows/MyVulkanApp_Installer.exe + MyVulkanApp-macOS/MyVulkanApp.dmg +---- + +=== Conclusion + +Packaging and distribution are critical steps in the lifecycle of a Vulkan application. By carefully considering platform-specific requirements, handling dependencies appropriately, and automating the packaging process, you can ensure a smooth experience for your users across different platforms. + +Remember that the goal of packaging is to make installation and updates as seamless as possible for your users. Invest time in creating a robust packaging and distribution system, and your users will benefit from a more professional and reliable application. + +In the next and final section, we'll summarize what we've learned throughout this chapter on tooling for Vulkan applications. + +link:05_extensions.adoc[Previous: Vulkan Extensions for Robustness] | link:07_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc new file mode 100644 index 00000000..75013f99 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc @@ -0,0 +1,239 @@ +::pp: {plus}{plus} + += Tooling: Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Conclusion + +In this chapter, we've explored a comprehensive set of tools and techniques for developing, debugging, and distributing Vulkan applications. Let's summarize what we've learned and discuss how to apply these techniques in your own projects. + +=== What We've Learned + +==== CI/CD for Vulkan Projects + +We started by implementing a continuous integration and continuous deployment pipeline specifically tailored for Vulkan applications. This included: + +* Setting up a basic CI/CD pipeline with GitHub Actions +* Handling Vulkan-specific considerations like SDK installation and GPU availability +* Automating testing for Vulkan applications +* Packaging and distributing Vulkan applications across different platforms + +A well-designed CI/CD pipeline helps ensure consistent quality across builds and platforms, catching issues early in the development process. + +==== Debugging with VK_KHR_debug_utils and RenderDoc + +We then explored powerful debugging tools for Vulkan applications: + +* Using the VK_KHR_debug_utils extension for in-application debugging +* Labeling objects, command buffers, and queue operations for better debugging +* Integrating RenderDoc for frame capture and analysis +* Combining these approaches for comprehensive debugging + +These tools provide visibility into the complex operations happening on the GPU, making it easier to identify and fix issues in your rendering pipeline. + +==== Crash Handling and Minidumps + +Next, we implemented robust crash handling mechanisms: + +* Basic crash handling for exceptions and signals +* Generating minidumps for detailed crash analysis +* Collecting Vulkan-specific information in crash reports +* Integrating with telemetry systems for production applications + +Proper crash handling helps you diagnose and fix issues that occur in production environments, leading to a more stable and reliable application. + +==== Vulkan Extensions for Robustness + +Finally, we explored Vulkan extensions that can help make your application more resilient to undefined behavior: + +* Using VK_EXT_robustness2 for handling out-of-bounds accesses and null descriptors +* Implementing other robustness extensions like VK_KHR_buffer_device_address and VK_EXT_descriptor_indexing +* Combining robustness extensions with debugging tools for maximum effectiveness + +These extensions provide valuable tools for making your application more robust against common errors, though they should not be seen as a substitute for fixing the underlying issues. + +=== Putting It All Together + +Throughout this chapter, we've used modern C++20 modules and the vk::raii namespace for resource management. This approach offers several advantages: + +* Improved code organization with modules +* Automatic resource cleanup with RAII +* More readable and maintainable code +* Better error handling with exceptions + +Let's see how all these components can work together in a complete application: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class VulkanApplication { +public: + VulkanApplication() { + // Initialize crash handler + crash_handler::initialize("MyVulkanApp", "crash_logs"); + + // Initialize Vulkan with debugging and robustness + initialize_vulkan(); + } + + void run() { + // Main application loop + while (!should_close()) { + try { + update(); + render(); + } catch (const vk::SystemError& e) { + // Handle recoverable Vulkan errors + std::cerr << "Vulkan error: " << e.what() << std::endl; + if (!recover_from_error()) { + break; + } + } + } + + cleanup(); + } + +private: + void initialize_vulkan() { + // Create instance with validation layers in debug builds + #ifdef _DEBUG + enable_validation_layers = true; + #else + enable_validation_layers = false; + #endif + + // Create instance + instance = create_instance(); + + // Set up debug messenger if validation is enabled + if (enable_validation_layers) { + debug_messenger = create_debug_messenger(instance); + } + + // Select physical device + physical_device = select_physical_device(instance); + + // Check for robustness support + has_robustness2 = check_robustness2_support(physical_device); + + // Create logical device with robustness if available + device = create_device(physical_device); + + // Name Vulkan objects for debugging + if (enable_validation_layers) { + debug_utils::set_name(device, *device, "Main Device"); + // Name other objects as they're created + } + + // Initialize other Vulkan resources + // ... + } + + void render() { + // Begin frame + auto cmd_buffer = begin_frame(); + + // Label command buffer regions for debugging + if (enable_validation_layers) { + vk::DebugUtilsLabelEXT label_info{}; + label_info.setPLabelName("Main Render Pass"); + label_info.setColor(std::array{0.0f, 1.0f, 0.0f, 1.0f}); + cmd_buffer.beginDebugUtilsLabelEXT(label_info); + } + + // Record rendering commands + // ... + + // End debug label + if (enable_validation_layers) { + cmd_buffer.endDebugUtilsLabelEXT(); + } + + // End frame + end_frame(cmd_buffer); + + // Capture frame with RenderDoc if requested + if (capture_next_frame) { + if (renderdoc_api) { + renderdoc_api->TriggerCapture(); + } + capture_next_frame = false; + } + } + + // Vulkan objects + vk::raii::Context context; + vk::raii::Instance instance{nullptr}; + vk::raii::DebugUtilsMessengerEXT debug_messenger{nullptr}; + vk::raii::PhysicalDevice physical_device{nullptr}; + vk::raii::Device device{nullptr}; + + // Flags + bool enable_validation_layers = false; + bool has_robustness2 = false; + bool capture_next_frame = false; + + // RenderDoc API + RENDERDOC_API_1_4_1* renderdoc_api = nullptr; +}; +---- + +=== Best Practices for Professional Vulkan Development + +Based on what we've covered in this chapter, here are some best practices for professional Vulkan development: + +1. *Automate Your Workflow*: Use CI/CD pipelines to automate building, testing, and packaging your application. + +2. *Debug Early and Often*: Use validation layers and debugging tools throughout development, not just when issues arise. + +3. *Name Your Objects*: Use VK_KHR_debug_utils to give meaningful names to Vulkan objects, making debugging much easier. + +4. *Prepare for Crashes*: Implement robust crash handling and reporting mechanisms from the start of your project. + +5. *Consider Robustness*: Evaluate the trade-offs of using robustness extensions based on your application's needs. + +6. *Test Across Platforms*: Vulkan applications can behave differently across different hardware and drivers, so test extensively. + +7. *Document Your Requirements*: Clearly document which Vulkan extensions and features your application requires. + +8. *Stay Updated*: The Vulkan ecosystem is constantly evolving, so stay informed about new extensions and tools. + +=== Future Directions + +As Vulkan continues to evolve, new tools and techniques will emerge for developing, debugging, and distributing applications. Some areas to watch include: + +* *Improved Debugging Tools*: Tools like RenderDoc continue to add new features for Vulkan debugging. +* *Ray Tracing Tooling*: As ray tracing becomes more common, expect more specialized tools for debugging and optimizing ray tracing pipelines. +* *Machine Learning Integration*: Tools that use machine learning to identify potential issues or optimize performance. +* *Cross-API Development*: Tools that help manage development across multiple graphics APIs (Vulkan, DirectX, Metal). + +=== Final Thoughts + +Developing professional Vulkan applications requires more than just understanding the API—it requires a comprehensive tooling ecosystem that supports the entire development lifecycle. By implementing the tools and techniques covered in this chapter, you'll be well-equipped to develop, debug, and distribute high-quality Vulkan applications. + +Remember that tooling is an investment that pays dividends throughout the development process. Time spent setting up good CI/CD pipelines, debugging tools, and crash handling mechanisms will save you countless hours of troubleshooting and manual work later on. + +=== Code Examples + +The complete code for this chapter can be found in the following files: + +* `simple_engine/32_cicd_setup.cpp`: Implementation of CI/CD for Vulkan projects +* `simple_engine/33_debug_utils.cpp`: Implementation of debugging with VK_KHR_debug_utils and RenderDoc +* `simple_engine/34_crash_handling.cpp`: Implementation of crash handling and minidumps +* `simple_engine/35_robustness_extensions.cpp`: Implementation of Vulkan extensions for robustness + +link:../../attachments/simple_engine/32_cicd_setup.cpp[CI/CD Setup C{pp} code] +link:../../attachments/simple_engine/33_debug_utils.cpp[Debug Utils C{pp} code] +link:../../attachments/simple_engine/34_crash_handling.cpp[Crash Handling C{pp} code] +link:../../attachments/simple_engine/35_robustness_extensions.cpp[Robustness Extensions C{pp} code] + +link:06_packaging_and_distribution.adoc[Previous: Packaging and Distribution] | link:../Mobile_Development/01_introduction.adoc[Next: Mobile Development] diff --git a/en/Building_a_Simple_Engine/Tooling/index.adoc b/en/Building_a_Simple_Engine/Tooling/index.adoc new file mode 100644 index 00000000..d781357a --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/index.adoc @@ -0,0 +1,22 @@ +::pp: {plus}{plus} + += Tooling +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +This chapter covers essential tooling and techniques for developing, debugging, and distributing Vulkan applications, with a focus on using modern C++20 modules and the vk::raii namespace. + +* link:01_introduction.adoc[Introduction] +* link:02_cicd.adoc[CI/CD for Vulkan Projects] +* link:03_debugging_and_renderdoc.adoc[Debugging with VK_KHR_debug_utils and RenderDoc] +* link:04_crash_minidump.adoc[Crash Handling and Minidumps] +* link:05_extensions.adoc[Vulkan Extensions for Robustness] +* link:06_packaging_and_distribution.adoc[Packaging and Distribution] +* link:07_conclusion.adoc[Conclusion] + +link:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc new file mode 100644 index 00000000..dc43e615 --- /dev/null +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -0,0 +1,62 @@ +:pp: {plus}{plus} + += Building a Simple Engine: Introduction +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c{pp} + +== Introduction + +Welcome to the "Building a Simple Engine" tutorial series! This series marks a transition from the foundational Vulkan concepts covered in the previous chapters to a more structured approach focused on building a reusable rendering engine. + +=== A New Learning Approach + +While the previous tutorial series focused on introducing individual Vulkan concepts step by step, this series takes a different approach: + +* *Intermediate to Advanced Level* - This tutorial assumes you have completed the original Vulkan tutorial series and are comfortable with the fundamental concepts. If this feels too advanced, start with the foundational tutorial and come back when you're ready. + +* *Concept-Focused Learning* - Rather than providing exhaustive details on every possible implementation approach, we focus on key architectural concepts and design patterns for building a rendering engine. + +* *More Independent Work* - This series will require more time and independent problem-solving from you. We'll provide the framework and guidance, but you'll need to fill in some details on your own. Remember that you have access to many resources: the https://docs.vulkan.org/guide/latest/[Vulkan Guide] provides in-depth information on topics this tutorial only introduces, the https://docs.vulkan.org/samples/latest/[Samples] offer hands-on demonstrations of specific implementations, and the https://docs.vulkan.org/spec/latest/[Specification] contains the precise rules of the API. If you need assistance, don't hesitate to reach out—Vulkan has a vibrant community with experts across a wide range of topics. + +=== What to Expect + +The "Building a Simple Engine" series is designed as a starting point for your journey into engine development, not a finishing point. We'll cover: + +1. *Engine Architecture* - How to structure your code for flexibility, maintainability, and extensibility. + +2. *Resource Management* - More sophisticated approaches to handling models, textures, and other assets. + +3. *Rendering Techniques* - Implementation of modern rendering approaches within an engine framework. + +4. *Performance Considerations* - How to design your engine with performance in mind. + +5. *Publication Considerations* - How to prepare your application for distribution in a professional environment, including packaging, deployment, and platform-specific considerations. + +Throughout this series, we encourage you to experiment, extend the provided examples, and even challenge some of our design decisions. The best way to learn engine development is by doing, and sometimes by making (and learning from) mistakes. + +=== How to Use This Tutorial + +Each chapter in this series builds upon the previous ones, gradually constructing a simple but functional rendering engine. We recommend: + +1. Read through each chapter completely before implementing the code. +2. Take time to understand the concepts before moving on. +3. Don't hesitate to revisit the original Vulkan tutorial if you need to refresh your understanding of specific Vulkan concepts. +4. Experiment with the code and try to extend it with your own features. + +Let's begin our journey into engine development with these chapters: + +1. link:Engine_Architecture/01_introduction.adoc[Engine Architecture] - How to structure your code for flexibility, maintainability, and extensibility. +2. link:Camera_Transformations/01_introduction.adoc[Camera Transformations] - Implementation of camera systems and transformations. +3. link:Lighting_Materials/01_introduction.adoc[Lighting & Materials] - Basic lighting models and push constants. +4. link:GUI/01_introduction.adoc[GUI] - Implementation of a graphical user interface using Dear ImGui. +5. link:Loading_Models/01_introduction.adoc[Loading Models] - More sophisticated approaches to handling models, textures, and other assets. +6. link:Subsystems/01_introduction.adoc[Subsystems] - Implementation of Audio and Physics subsystems with Vulkan compute capabilities. +7. link:Tooling/01_introduction.adoc[Tooling] - CI/CD, Debugging, Crash minidump, Distribution, and Vulkan extensions for robustness. +8. link:Mobile_Development/01_introduction.adoc[Mobile Development] - Adapting the engine for Android/iOS, focusing on performance considerations and mobile-specific Vulkan extensions. + +link:../conclusion.adoc[Previous: Main Tutorial Conclusion] | link:Engine_Architecture/01_introduction.adoc[Next: Engine Architecture] diff --git a/en/conclusion.adoc b/en/conclusion.adoc new file mode 100644 index 00000000..5608fe28 --- /dev/null +++ b/en/conclusion.adoc @@ -0,0 +1,72 @@ +:pp: {plus}{plus} + += Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c{pp} + +== Conclusion + +Congratulations on completing the Core Vulkan tutorial series! You've built a + solid foundation in Vulkan development that will serve you well in your graphics programming journey. + +=== What You've Learned + +Throughout this tutorial series, you've gained knowledge and practical experience in: + +1. *Vulkan Fundamentals* - Understanding the core concepts of Vulkan, its architecture, and how it differs from other graphics APIs. + +2. *Setting Up a Vulkan Application* - Creating instances, selecting physical devices, creating logical devices, and establishing the rendering pipeline. + +3. *Drawing Operations* - Rendering triangles, working with vertex buffers, and understanding the Vulkan rendering process. + +4. *Advanced Rendering Techniques* - Implementing depth buffering, texture mapping, mipmaps, and multisampling. + +5. *Asset Management* - Loading 3D models and textures for use in your Vulkan applications. + +6. *Performance Optimization* - Using compute shaders and multithreading to improve application performance. + +7. *Ecosystem Integration* - Working with utilities, ensuring compatibility, and understanding Vulkan profiles. + +8. *Platform-Specific Development* - Adapting your Vulkan application for Android. + +9. *Modern Graphics Techniques* - Migrating to glTF and KTX2 formats for improved asset management. + +=== Where to Go From Here + +This tutorial series has provided you with the essential tools for more +advanced Vulkan development. With these fundamentals mastered, you're now ready to explore more complex topics and build more sophisticated applications. + +Future tutorial series will build upon these fundamentals to help you create +more structured and reusable rendering solutions. Future, planned +tutorials will assume you've completed all the chapters in this series and +will guide you through implementing more sophisticated rendering techniques +and architectures. + +Remember that Vulkan development is a continuous learning process. The graphics programming landscape is constantly evolving, and there's always more to learn and explore. + +=== Community Resources and Getting Help + +As you continue your Vulkan journey, you may encounter challenges or have questions. Fortunately, there are several active communities where you can get help: + +1. *Khronos Slack* - Join the official Khronos Group Slack workspace and the #vulkan channel for direct interaction with Vulkan developers and experts. You can get an invitation at https://khr.io/slack[khr.io/slack]. + +2. *Vulkan Discord* - The community-run Vulkan Discord server is a great place for real-time discussions, troubleshooting, and connecting with other Vulkan developers. Join at https://discord.gg/vulkan[discord.gg/vulkan]. + +3. *Reddit* - The r/vulkan subreddit (https://www.reddit.com/r/vulkan/[reddit.com/r/vulkan]) is an active community for sharing news, asking questions, and discussing Vulkan development. + +4. *Stack Overflow* - For specific programming questions, use the +https://stackoverflow.com/questions/tagged/vulkan[vulkan] tag on Stack Overflow. + +5. *Vulkan Specification* - When in doubt, refer to the official https://docs.vulkan.org/spec/latest/[Vulkan Specification] for authoritative information. + +Don't hesitate to reach out to these communities - they're filled with developers who are passionate about Vulkan and eager to help others succeed. + +Thank you for following along with this tutorial series. You've taken a big +first step in a long journey. + +link:17_Multithreading.adoc[Previous: Multithreading] | link:Building_a_Simple_Engine/introduction.adoc[Next: Building a Simple Engine] From 7e419d53a9d15db0ae76f552d9898689fdb28216 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 14 Jul 2025 12:18:03 -0700 Subject: [PATCH 002/102] Refactor GPU optimization sections to generalize vendor-specific recommendations - Consolidated sections to address vendor-agnostic optimizations for mobile GPUs. - Replaced Huawei-specific details with broader guidance applicable to various mobile GPU architectures (Mali, Adreno, PowerVR, etc.). - Improved Vulkan extension examples and added checks for multiple vendor IDs for better compatibility. - Updated best practices and performance tuning sections to emphasize device diversity and testing requirements. --- .../02_platform_considerations.adoc | 33 +++++++------ .../03_performance_optimizations.adoc | 44 ++++++++++------- .../05_vulkan_extensions.adoc | 47 ++++++++++++------- .../Mobile_Development/06_conclusion.adoc | 8 ++-- 4 files changed, 80 insertions(+), 52 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc index 57b21031..284da385 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc @@ -72,33 +72,38 @@ Android input handling differs from desktop: 2. *Sensors*: Android devices have various sensors (accelerometer, gyroscope, etc.) that you can use for input. -==== Huawei Device Considerations +==== Vendor-Specific Considerations -Huawei devices represent a significant portion of the Android market and have some specific considerations: +Different Android device manufacturers may have specific considerations: -1. *HarmonyOS/EMUI*: Huawei's custom operating system is based on Android but has some differences. Test your application specifically on Huawei devices to ensure compatibility. +1. *Custom Android Versions*: Many manufacturers use customized versions of Android. Test your application on various devices to ensure compatibility. -2. *Huawei GPU Turbo*: This technology optimizes GPU performance and power efficiency. Your Vulkan application can benefit from this, but you should test to ensure compatibility. +2. *GPU Architectures*: Different vendors use different GPU architectures (Adreno, Mali, PowerVR, etc.). Each has unique performance characteristics. -3. *AppGallery Distribution*: Since Huawei devices may not have Google Play Services, consider distributing your application through Huawei's AppGallery. +3. *Alternative App Stores*: Some devices may not have Google Play Services. Consider distributing through alternative app stores when necessary. -4. *Kirin SoCs*: Huawei's Kirin processors use Mali GPUs which are tile-based renderers. Optimize your rendering pipeline accordingly using the techniques described in the TBR section. +4. *SoC Variations*: System-on-Chip variations affect performance. Most mobile GPUs are tile-based renderers, so optimize your rendering pipeline accordingly using the techniques described in the TBR section. [source,cpp] ---- -// Example of checking for Huawei devices -bool is_huawei_device(vk::PhysicalDevice physical_device) { +// Example of checking for specific device vendors +bool check_device_vendor(vk::PhysicalDevice physical_device, uint32_t vendor_id) { vk::PhysicalDeviceProperties props = physical_device.getProperties(); - return props.vendorID == 0x19E5; // Huawei vendor ID + return props.vendorID == vendor_id; } -// You can then apply Huawei-specific optimizations if needed +// Common vendor IDs +const uint32_t VENDOR_ID_QUALCOMM = 0x5143; // Adreno +const uint32_t VENDOR_ID_ARM = 0x13B5; // Mali +const uint32_t VENDOR_ID_IMAGINATION = 0x1010; // PowerVR +const uint32_t VENDOR_ID_HUAWEI = 0x19E5; // Kirin + +// You can then apply vendor-specific optimizations if needed void configure_for_device(vk::PhysicalDevice physical_device) { - if (is_huawei_device(physical_device)) { - // Apply Huawei-specific optimizations - // For example, you might want to use specific memory allocation strategies - // or enable certain features that work well on Huawei's implementation + if (check_device_vendor(physical_device, VENDOR_ID_HUAWEI)) { + // Apply Huawei-specific optimizations if needed } + // Handle other vendors as needed } ---- diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 53d02ac3..123f59b0 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -305,36 +305,46 @@ Mobile GPUs are particularly sensitive to draw call overhead: Different mobile GPU vendors have specific architectures that may benefit from targeted optimizations. -==== Huawei Kirin GPU Optimizations +==== Vendor-Specific GPU Optimizations -Huawei's Kirin SoCs typically use ARM Mali GPUs, but with custom configurations and optimizations: +Different mobile GPU vendors have specific architectures that benefit from targeted optimizations: -1. *GPU Turbo Technology*: Huawei's GPU Turbo technology can significantly improve performance and power efficiency. To take advantage of this: +1. *GPU Technologies*: Many vendors implement custom GPU technologies to improve performance and power efficiency: - Maintain stable frame rates rather than pushing for maximum frames - Avoid unnecessary GPU state changes - - Use efficient rendering techniques that work well with Mali GPUs + - Use efficient rendering techniques appropriate for the GPU architecture -2. *Memory Management*: Kirin SoCs often have unified memory architecture: +2. *Memory Management*: Many mobile SoCs have unified memory architecture: - Use `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` memory when possible - - Take advantage of the fast CPU-GPU memory transfers + - Take advantage of fast CPU-GPU memory transfers in unified memory architectures -3. *Texture Compression*: Huawei devices support ASTC texture compression: +3. *Texture Compression*: Different devices support different texture compression formats: [source,cpp] ---- -// Check for ASTC support on Huawei devices -bool supports_astc(vk::PhysicalDevice physical_device) { - vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); - return features.textureCompressionASTC_LDR; +// Check for texture compression format support +bool supports_texture_format(vk::PhysicalDevice physical_device, vk::Format format) { + vk::FormatProperties props = physical_device.getFormatProperties(format); + return (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage); } -// Prioritize ASTC for Huawei devices +// Get optimal texture format based on device capabilities vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) { vk::PhysicalDeviceProperties props = physical_device.getProperties(); + vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); - // If it's a Huawei device and supports ASTC, prioritize it - if (props.vendorID == 0x19E5 && supports_astc(physical_device)) { - return vk::Format::eAstc8x8SrgbBlock; // Or another ASTC format based on your needs + // Check for ASTC support (widely supported on modern mobile GPUs) + if (features.textureCompressionASTC_LDR) { + return vk::Format::eAstc8x8SrgbBlock; + } + + // Check for vendor-specific optimizations + // Huawei devices (Mali GPUs) + if (props.vendorID == 0x19E5) { + // Check for ETC2 support as fallback + if (supports_texture_format(physical_device, vk::Format::eEtc2R8G8B8A8SrgbBlock)) { + return vk::Format::eEtc2R8G8B8A8SrgbBlock; + } } // Otherwise, fall back to the general format selection @@ -342,11 +352,11 @@ vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) { } ---- -4. *Performance Monitoring*: Huawei provides performance monitoring tools that can help identify bottlenecks specific to their hardware. +4. *Performance Monitoring*: Most vendors provide performance monitoring tools that can help identify bottlenecks specific to their hardware. === Best Practices for Mobile Performance -1. *Profile on Target Devices*: Performance characteristics vary widely across mobile devices. Test on a range of hardware, including different Huawei models. +1. *Profile on Target Devices*: Performance characteristics vary widely across mobile devices. Test on a range of hardware from different manufacturers and with different GPU architectures. 2. *Monitor Temperature*: Mobile devices throttle performance when they get hot. Design your engine to adapt to thermal throttling. diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc index a2339301..e28ee91a 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -259,27 +259,40 @@ vk::Device device = physical_device.createDevice(device_create_info); Different mobile vendors may have varying levels of support for Vulkan extensions. Understanding these differences can help you optimize your application for specific hardware. -==== Huawei Extension Support +==== Vendor-Specific Extension Support Details -Huawei devices, particularly those with newer Kirin SoCs, have strong support for Vulkan extensions that can significantly improve performance: +Different mobile GPU vendors have varying levels of support for Vulkan extensions: -1. *Dynamic Rendering Support*: Huawei's implementation of `VK_KHR_dynamic_rendering` is highly optimized for their Mali-based GPUs. This can lead to significant performance improvements compared to traditional render passes. +1. *Dynamic Rendering Support*: Many mobile GPUs have optimized implementations of `VK_KHR_dynamic_rendering`. This can lead to significant performance improvements compared to traditional render passes, especially on tile-based renderers. -2. *Tile-Based Optimizations*: As Huawei devices use tile-based renderers, extensions like `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are particularly effective. These extensions can reduce memory bandwidth by up to 30% in some scenarios on Huawei hardware. +2. *Tile-Based Optimizations*: For devices with tile-based renderers (including Mali, PowerVR, and many others), extensions like `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are particularly effective. These extensions can reduce memory bandwidth by up to 30% in some scenarios. -3. *Checking for Huawei-Specific Support*: +3. *Checking for Vendor-Specific Extension Support*: [source,cpp] ---- -bool check_huawei_extension_support(vk::PhysicalDevice physical_device) { +// Common vendor IDs +const uint32_t VENDOR_ID_QUALCOMM = 0x5143; // Adreno +const uint32_t VENDOR_ID_ARM = 0x13B5; // Mali +const uint32_t VENDOR_ID_IMAGINATION = 0x1010; // PowerVR +const uint32_t VENDOR_ID_HUAWEI = 0x19E5; // Kirin +const uint32_t VENDOR_ID_APPLE = 0x106B; // Apple + +bool check_vendor_extension_support(vk::PhysicalDevice physical_device) { vk::PhysicalDeviceProperties props = physical_device.getProperties(); - bool is_huawei = (props.vendorID == 0x19E5); - - if (!is_huawei) { - return false; + std::string vendor_name; + + // Identify vendor + switch (props.vendorID) { + case VENDOR_ID_QUALCOMM: vendor_name = "Qualcomm"; break; + case VENDOR_ID_ARM: vendor_name = "ARM Mali"; break; + case VENDOR_ID_IMAGINATION: vendor_name = "PowerVR"; break; + case VENDOR_ID_HUAWEI: vendor_name = "Huawei"; break; + case VENDOR_ID_APPLE: vendor_name = "Apple"; break; + default: vendor_name = "Unknown"; break; } - // Check for extensions that work particularly well on Huawei devices + // Check for extensions that work well on mobile devices auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); bool has_dynamic_rendering = false; bool has_dynamic_rendering_local_read = false; @@ -297,7 +310,7 @@ bool check_huawei_extension_support(vk::PhysicalDevice physical_device) { } // Log the extension support - std::cout << "Huawei device detected with extension support:" << std::endl; + std::cout << vendor_name << " device detected with extension support:" << std::endl; std::cout << " Dynamic Rendering: " << (has_dynamic_rendering ? "Yes" : "No") << std::endl; std::cout << " Dynamic Rendering Local Read: " << (has_dynamic_rendering_local_read ? "Yes" : "No") << std::endl; std::cout << " Shader Tile Image: " << (has_shader_tile_image ? "Yes" : "No") << std::endl; @@ -306,10 +319,10 @@ bool check_huawei_extension_support(vk::PhysicalDevice physical_device) { } ---- -4. *Huawei-Specific Optimizations*: When developing for Huawei devices, consider these optimizations: - - Prioritize the use of dynamic rendering over traditional render passes +4. *Vendor-Specific Optimizations*: When developing for mobile devices, consider these optimizations: + - Prioritize the use of dynamic rendering over traditional render passes on tile-based renderers - Use tile-based extensions whenever available - - Test different configurations to find the optimal settings for specific Huawei models + - Test different configurations to find the optimal settings for various device models === Best Practices for Using Extensions @@ -317,9 +330,9 @@ bool check_huawei_extension_support(vk::PhysicalDevice physical_device) { 2. *Fallback Paths*: Implement fallback paths for when extensions aren't available. -3. *Test on Real Devices*: Extensions may behave differently across vendors and devices, particularly between different Huawei models. +3. *Test on Real Devices*: Extensions may behave differently across vendors and devices. Test on a variety of hardware from different manufacturers. -4. *Stay Updated*: Keep track of new extensions that could benefit mobile performance, especially as Huawei continues to enhance their Vulkan support. +4. *Stay Updated*: Keep track of new extensions that could benefit mobile performance, as mobile GPU vendors continue to enhance their Vulkan support. In the next section, we'll conclude our exploration of mobile development with a summary of key takeaways and best practices. diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc index 955dd4c7..11b25ade 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -24,7 +24,7 @@ We started by examining the specific requirements and constraints of developing * Managing the complex lifecycle of mobile applications * Handling platform-specific input methods * Creating cross-platform abstractions to maintain a single codebase -* Addressing Huawei-specific considerations like HarmonyOS/EMUI and AppGallery distribution +* Addressing vendor-specific considerations like custom Android versions and alternative app stores Understanding these platform-specific considerations is essential for creating a robust mobile application that behaves correctly across different devices and operating systems. @@ -37,7 +37,7 @@ We then explored key performance optimizations for mobile hardware: * Minimizing memory allocations through pooling and suballocation * Reducing bandwidth usage with optimized vertex formats and smaller data types * Optimizing draw calls through instancing, batching, and LOD systems -* Leveraging vendor-specific optimizations, particularly for Huawei's Kirin SoCs with GPU Turbo technology +* Leveraging vendor-specific optimizations for different mobile GPU architectures These optimizations help address the limited resources available on mobile devices, ensuring your application runs smoothly while conserving battery life. @@ -62,7 +62,7 @@ Finally, we explored Vulkan extensions that can significantly improve performanc * VK_EXT_shader_tile_image: Providing direct access to tile memory in shaders * Combining these extensions for maximum performance * Best practices for using extensions in a cross-platform engine -* Vendor-specific extension support, with particular focus on Huawei's optimized implementation of these extensions +* Vendor-specific extension support and how different mobile GPU vendors implement these extensions These extensions leverage the unique characteristics of mobile GPUs, particularly tile-based renderers, to achieve better performance and power efficiency. @@ -251,7 +251,7 @@ Mobile graphics hardware continues to evolve rapidly. Here are some trends to wa * *Ray Tracing on Mobile*: As ray tracing hardware becomes more common on mobile devices, new optimization techniques will emerge. * *AI-Enhanced Rendering*: Mobile GPUs are increasingly incorporating AI acceleration, which can be leveraged for rendering tasks. * *Cross-Platform Development*: Tools and frameworks for cross-platform development continue to improve, making it easier to target multiple platforms. -* *Huawei's GPU Innovations*: Huawei continues to advance their GPU technology with each generation of Kirin SoCs, introducing new features and optimizations that can be leveraged through Vulkan. +* *Mobile GPU Innovations*: Mobile GPU vendors continue to advance their technologies with each generation, introducing new features and optimizations that can be leveraged through Vulkan. === Final Thoughts From 4644950055532a3ee4e887b6f2d561013a87d3b8 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 14 Jul 2025 12:44:53 -0700 Subject: [PATCH 003/102] Refactor mobile GPU optimization sections and enhance Vulkan extension details - Simplified and reorganized GPU optimization lists for readability. - Expanded explanations for `VK_KHR_dynamic_rendering_local_read` and `VK_EXT_shader_tile_image` to highlight memory bandwidth reduction benefits. - Improved consistency in vendor-specific and tile-based optimization recommendations. - Added detailed examples and practical use cases for extension advantages in mobile rendering pipelines. --- .../03_performance_optimizations.adoc | 10 +++-- .../05_vulkan_extensions.adoc | 43 ++++++++++++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 123f59b0..0903c4ff 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -309,16 +309,17 @@ Different mobile GPU vendors have specific architectures that may benefit from t Different mobile GPU vendors have specific architectures that benefit from targeted optimizations: -1. *GPU Technologies*: Many vendors implement custom GPU technologies to improve performance and power efficiency: +* *GPU Technologies*: Many vendors implement custom GPU technologies to improve performance and power efficiency: - Maintain stable frame rates rather than pushing for maximum frames - Avoid unnecessary GPU state changes - Use efficient rendering techniques appropriate for the GPU architecture -2. *Memory Management*: Many mobile SoCs have unified memory architecture: +* *Memory Management*: Many mobile SoCs have unified memory architecture: - Use `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` memory when possible - Take advantage of fast CPU-GPU memory transfers in unified memory architectures -3. *Texture Compression*: Different devices support different texture compression formats: +* *Texture Compression*: Different devices support different texture +compression formats: [source,cpp] ---- @@ -352,7 +353,8 @@ vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) { } ---- -4. *Performance Monitoring*: Most vendors provide performance monitoring tools that can help identify bottlenecks specific to their hardware. +* *Performance Monitoring*: Most vendors provide performance monitoring tools + that can help identify bottlenecks specific to their hardware. === Best Practices for Mobile Performance diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc index e28ee91a..17c334f7 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -125,10 +125,22 @@ This extension enhances dynamic rendering by allowing fragment shaders to read f Key benefits include: -1. *Reduced Memory Bandwidth*: Reads happen from on-chip memory rather than main memory. +1. *Reduced Memory Bandwidth*: Reads happen from on-chip memory rather than main memory, reducing bandwidth usage by up to 30% in bandwidth-intensive operations. 2. *Improved Performance*: Particularly for algorithms that need to read from previously written attachments. 3. *Power Efficiency*: Lower memory bandwidth means lower power consumption. +==== How It Reduces Memory Bandwidth + +The `VK_KHR_dynamic_rendering_local_read` extension is particularly effective at reducing memory bandwidth because: + +1. *Eliminates Tile Flush Operations*: Without this extension, when a shader needs to read from a previously written attachment, the GPU must flush the entire tile to main memory and then read it back. This extension allows the shader to read directly from the tile memory, eliminating these costly flush operations. + +2. *Optimizes Post-Processing Effects*: Effects like bloom, tone mapping, and depth-of-field that require reading from rendered images can be performed without leaving the tile memory. + +3. *Bandwidth Reduction Measurements*: In real-world applications, this extension has been shown to reduce memory bandwidth by up to 30% for post-processing heavy workloads. This is particularly significant on mobile devices where memory bandwidth is often a bottleneck and directly impacts battery life. + +4. *Practical Example*: Consider a deferred rendering pipeline that needs to read G-buffer data. Without this extension, the G-buffer would need to be written to main memory and then read back for the lighting pass. With this extension, the lighting pass can read directly from the G-buffer in tile memory, saving significant bandwidth. + ==== Implementation To use this extension: @@ -187,6 +199,24 @@ This extension allows shaders to: 1. *Access Tile Memory Directly*: Read and write to the current tile's memory without going through main memory. 2. *Perform Tile-Local Operations*: Execute operations that stay entirely within the tile memory. 3. *Optimize Bandwidth-Intensive Algorithms*: Particularly beneficial for post-processing effects. +4. *Reduce Memory Bandwidth*: Can reduce memory bandwidth usage by up to 30% for rendering workloads that involve multiple passes. + +==== How It Reduces Memory Bandwidth + +The `VK_EXT_shader_tile_image` extension is particularly effective at reducing memory bandwidth for these reasons: + +1. *Tile-Based Architecture Optimization*: Mobile GPUs typically use tile-based rendering, where the screen is divided into small tiles that are processed independently. This extension takes full advantage of this architecture by allowing shaders to work directly with the tile data in fast on-chip memory. + +2. *Eliminates Intermediate Memory Transfers*: Without this extension, multi-pass rendering requires writing results to main memory after each pass and reading them back for the next pass. With `VK_EXT_shader_tile_image`, these intermediate results can stay in tile memory, eliminating these costly transfers. + +3. *Bandwidth Savings Measurements*: Testing on various mobile GPUs has shown memory bandwidth reductions of up to 30% for complex rendering pipelines that use multiple passes, such as those involving post-processing effects. + +4. *Practical Applications*: + - *Image Processing Filters*: Applying multiple filters (blur, sharpen, color correction) can be done without leaving tile memory. + - *Deferred Rendering*: G-buffer data can be kept in tile memory for the lighting pass. + - *Shadow Mapping*: Shadow calculations can be performed more efficiently by keeping depth information in tile memory. + +5. *Power Efficiency*: The reduction in memory bandwidth directly translates to lower power consumption, which is critical for mobile devices. Tests have shown up to 20% power savings for graphics-intensive applications. ==== Implementation @@ -263,11 +293,13 @@ Different mobile vendors may have varying levels of support for Vulkan extension Different mobile GPU vendors have varying levels of support for Vulkan extensions: -1. *Dynamic Rendering Support*: Many mobile GPUs have optimized implementations of `VK_KHR_dynamic_rendering`. This can lead to significant performance improvements compared to traditional render passes, especially on tile-based renderers. +* *Dynamic Rendering Support*: Many mobile GPUs have optimized +implementations of `VK_KHR_dynamic_rendering`. This can lead to significant performance improvements compared to traditional render passes, especially on tile-based renderers. -2. *Tile-Based Optimizations*: For devices with tile-based renderers (including Mali, PowerVR, and many others), extensions like `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are particularly effective. These extensions can reduce memory bandwidth by up to 30% in some scenarios. +* *Tile-Based Optimizations*: For devices with tile-based renderers +(including Mali, PowerVR, and many others), extensions like `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are particularly effective. These extensions can reduce memory bandwidth by up to 30% in some scenarios. -3. *Checking for Vendor-Specific Extension Support*: +* *Checking for Vendor-Specific Extension Support*: [source,cpp] ---- @@ -319,7 +351,8 @@ bool check_vendor_extension_support(vk::PhysicalDevice physical_device) { } ---- -4. *Vendor-Specific Optimizations*: When developing for mobile devices, consider these optimizations: +* *Vendor-Specific Optimizations*: When developing for mobile devices, +consider these optimizations: - Prioritize the use of dynamic rendering over traditional render passes on tile-based renderers - Use tile-based extensions whenever available - Test different configurations to find the optimal settings for various device models From 2edbd757ae30c32f027294fb6ccd3ab8f8fb4a90 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 14 Jul 2025 13:06:01 -0700 Subject: [PATCH 004/102] Add detailed section on rendergraphs and synchronization in Vulkan - Expanded tutorial with a comprehensive explanation of rendergraphs, including resource management, dependency tracking, and synchronization benefits. - Added a sample `Rendergraph` class implementation with methods for resource and pass management, including compile and execute functions. - Included practical examples such as deferred renderer setup and Vulkan synchronization best practices using semaphores, fences, and pipeline barriers. - Enhanced Vulkan tutorial content with a focus on simplifying complex rendering workflows and optimizing synchronization. --- .../05_rendering_pipeline.adoc | 538 ++++++++++++++++++ 1 file changed, 538 insertions(+) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 1ca911b4..90eb2ecf 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -96,6 +96,544 @@ Modern rendering techniques often require multiple passes, each with a specific In this tutorial, we use Vulkan's dynamic rendering feature with vk::raii instead of traditional render passes. Dynamic rendering simplifies the rendering process by allowing us to begin and end rendering operations with a single command, without explicitly creating VkRenderPass and VkFramebuffer objects. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Additionally, our engine uses C++20 modules for better code organization, faster compilation times, and improved encapsulation. +=== Rendergraphs and Synchronization + +A rendergraph is a higher-level abstraction that represents the entire rendering process as a directed acyclic graph (DAG), where nodes are render passes and edges represent dependencies between them. This approach offers several advantages over traditional render pass management: + +==== What is a Rendergraph? + +A rendergraph is a data structure that: + +1. *Describes Resources*: Tracks all resources (textures, buffers) used in rendering. +2. *Defines Operations*: Specifies what operations (render passes) will be performed. +3. *Manages Dependencies*: Automatically determines the dependencies between operations. +4. *Handles Synchronization*: Automatically inserts necessary synchronization primitives. +5. *Optimizes Memory*: Can perform memory aliasing and other optimizations. + +[source,cpp] +---- +// A simple rendergraph class +class Rendergraph { +private: + struct Resource { + std::string name; + vk::Format format; + vk::Extent2D extent; + vk::ImageUsageFlags usage; + vk::ImageLayout initialLayout; + vk::ImageLayout finalLayout; + + // The actual Vulkan resources + vk::raii::Image image = nullptr; + vk::raii::DeviceMemory memory = nullptr; + vk::raii::ImageView view = nullptr; + }; + + struct Pass { + std::string name; + std::vector inputs; // Resources read by this pass + std::vector outputs; // Resources written by this pass + std::function executeFunc; + }; + + std::unordered_map resources; + std::vector passes; + std::vector executionOrder; // Indices into passes + + // Synchronization objects + std::vector semaphores; + std::vector> semaphoreSignalWaitPairs; // (signaling pass, waiting pass) + + vk::raii::Device& device; + +public: + explicit Rendergraph(vk::raii::Device& dev) : device(dev) {} + + // Add a resource to the rendergraph + void AddResource(const std::string& name, vk::Format format, vk::Extent2D extent, + vk::ImageUsageFlags usage, vk::ImageLayout initialLayout, + vk::ImageLayout finalLayout) { + Resource resource; + resource.name = name; + resource.format = format; + resource.extent = extent; + resource.usage = usage; + resource.initialLayout = initialLayout; + resource.finalLayout = finalLayout; + + resources[name] = resource; + } + + // Add a pass to the rendergraph + void AddPass(const std::string& name, + const std::vector& inputs, + const std::vector& outputs, + std::function executeFunc) { + Pass pass; + pass.name = name; + pass.inputs = inputs; + pass.outputs = outputs; + pass.executeFunc = executeFunc; + + passes.push_back(pass); + } + + // Compile the rendergraph + void Compile() { + // Build the dependency graph + std::vector> dependencies(passes.size()); + std::vector> dependents(passes.size()); + + // Map resources to the passes that write to them + std::unordered_map resourceWriters; + + // Find dependencies based on resource usage + for (size_t i = 0; i < passes.size(); ++i) { + const auto& pass = passes[i]; + + // Check inputs + for (const auto& input : pass.inputs) { + auto it = resourceWriters.find(input); + if (it != resourceWriters.end()) { + // This pass depends on the pass that writes to this resource + dependencies[i].push_back(it->second); + dependents[it->second].push_back(i); + } + } + + // Record outputs + for (const auto& output : pass.outputs) { + resourceWriters[output] = i; + } + } + + // Topological sort to determine execution order + std::vector visited(passes.size(), false); + std::vector inStack(passes.size(), false); + std::function visit = [&](size_t node) { + if (inStack[node]) { + throw std::runtime_error("Cycle detected in rendergraph"); + } + + if (visited[node]) { + return; + } + + inStack[node] = true; + + for (auto dependent : dependents[node]) { + visit(dependent); + } + + inStack[node] = false; + visited[node] = true; + executionOrder.push_back(node); + }; + + for (size_t i = 0; i < passes.size(); ++i) { + if (!visited[i]) { + visit(i); + } + } + + // Create synchronization objects + for (size_t i = 0; i < passes.size(); ++i) { + for (auto dep : dependencies[i]) { + // Create a semaphore for this dependency + semaphores.emplace_back(device.createSemaphore({})); + semaphoreSignalWaitPairs.emplace_back(dep, i); + } + } + + // Allocate actual resources + for (auto& [name, resource] : resources) { + // Create image + vk::ImageCreateInfo imageInfo; + imageInfo.setImageType(vk::ImageType::e2D) + .setFormat(resource.format) + .setExtent({resource.extent.width, resource.extent.height, 1}) + .setMipLevels(1) + .setArrayLayers(1) + .setSamples(vk::SampleCountFlagBits::e1) + .setTiling(vk::ImageTiling::eOptimal) + .setUsage(resource.usage) + .setSharingMode(vk::SharingMode::eExclusive) + .setInitialLayout(vk::ImageLayout::eUndefined); + + resource.image = device.createImage(imageInfo); + + // Allocate memory + vk::MemoryRequirements memRequirements = resource.image.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.setAllocationSize(memRequirements.size) + .setMemoryTypeIndex(FindMemoryType(memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eDeviceLocal)); + + resource.memory = device.allocateMemory(allocInfo); + resource.image.bindMemory(*resource.memory, 0); + + // Create image view + vk::ImageViewCreateInfo viewInfo; + viewInfo.setImage(*resource.image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(resource.format) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + + resource.view = device.createImageView(viewInfo); + } + } + + // Execute the rendergraph + void Execute(vk::raii::CommandBuffer& commandBuffer, vk::Queue queue) { + std::vector cmdBuffers; + std::vector waitSemaphores; + std::vector waitStages; + std::vector signalSemaphores; + + // For each pass in the execution order + for (auto passIdx : executionOrder) { + const auto& pass = passes[passIdx]; + + // Collect wait semaphores for this pass + waitSemaphores.clear(); + waitStages.clear(); + + for (size_t i = 0; i < semaphoreSignalWaitPairs.size(); ++i) { + if (semaphoreSignalWaitPairs[i].second == passIdx) { + waitSemaphores.push_back(*semaphores[i]); + waitStages.push_back(vk::PipelineStageFlagBits::eColorAttachmentOutput); + } + } + + // Collect signal semaphores for this pass + signalSemaphores.clear(); + + for (size_t i = 0; i < semaphoreSignalWaitPairs.size(); ++i) { + if (semaphoreSignalWaitPairs[i].first == passIdx) { + signalSemaphores.push_back(*semaphores[i]); + } + } + + // Begin command buffer + commandBuffer.begin({}); + + // Insert image memory barriers for layout transitions + for (const auto& input : pass.inputs) { + auto& resource = resources[input]; + + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(resource.initialLayout) + .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(*resource.image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) + .setSrcAccessMask(vk::AccessFlagBits::eMemoryWrite) + .setDstAccessMask(vk::AccessFlagBits::eShaderRead); + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, + vk::PipelineStageFlagBits::eFragmentShader, + vk::DependencyFlagBits::eByRegion, + 0, nullptr, + 0, nullptr, + 1, &barrier + ); + } + + for (const auto& output : pass.outputs) { + auto& resource = resources[output]; + + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(resource.initialLayout) + .setNewLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(*resource.image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) + .setSrcAccessMask(vk::AccessFlagBits::eMemoryRead) + .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlagBits::eByRegion, + 0, nullptr, + 0, nullptr, + 1, &barrier + ); + } + + // Execute the pass + pass.executeFunc(commandBuffer); + + // Insert image memory barriers for final layout transitions + for (const auto& output : pass.outputs) { + auto& resource = resources[output]; + + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setNewLayout(resource.finalLayout) + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(*resource.image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) + .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite) + .setDstAccessMask(vk::AccessFlagBits::eMemoryRead); + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlagBits::eByRegion, + 0, nullptr, + 0, nullptr, + 1, &barrier + ); + } + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo; + submitInfo.setWaitSemaphoreCount(static_cast(waitSemaphores.size())) + .setPWaitSemaphores(waitSemaphores.data()) + .setPWaitDstStageMask(waitStages.data()) + .setCommandBufferCount(1) + .setPCommandBuffers(&*commandBuffer) + .setSignalSemaphoreCount(static_cast(signalSemaphores.size())) + .setPSignalSemaphores(signalSemaphores.data()); + + queue.submit(1, &submitInfo, nullptr); + } + } + +private: + uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { + // Implementation to find suitable memory type + // ... + return 0; // Placeholder + } +}; +---- + +==== Vulkan Synchronization + +Vulkan provides several synchronization primitives to ensure correct execution order and memory visibility: + +1. *Semaphores*: Used for synchronization between queue operations (GPU-GPU synchronization). +2. *Fences*: Used for synchronization between CPU and GPU. +3. *Events*: Used for fine-grained synchronization within a command buffer. +4. *Barriers*: Used to synchronize access to resources and perform layout transitions. + +Proper synchronization is crucial in Vulkan because: + +1. *No Implicit Synchronization*: Unlike OpenGL, Vulkan doesn't provide implicit synchronization between operations. +2. *Parallel Execution*: Modern GPUs execute commands in parallel, which can lead to race conditions without proper synchronization. +3. *Memory Visibility*: Changes made by one operation may not be visible to another without proper barriers. + +===== Pipeline Barriers + +Pipeline barriers are one of the most important synchronization primitives in Vulkan. They ensure that operations before the barrier complete before operations after the barrier begin, and they can also perform layout transitions for images. + +[source,cpp] +---- +// Example of using a pipeline barrier for an image layout transition +void TransitionImageLayout(vk::raii::CommandBuffer& commandBuffer, + vk::Image image, + vk::Format format, + vk::ImageLayout oldLayout, + vk::ImageLayout newLayout) { + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(oldLayout) + .setNewLayout(newLayout) + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + + vk::PipelineStageFlags sourceStage; + vk::PipelineStageFlags destinationStage; + + if (oldLayout == vk::ImageLayout::eUndefined && + newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.setSrcAccessMask(vk::AccessFlagBits::eNone) + .setDstAccessMask(vk::AccessFlagBits::eTransferWrite); + + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eTransfer; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && + newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier.setSrcAccessMask(vk::AccessFlagBits::eTransferWrite) + .setDstAccessMask(vk::AccessFlagBits::eShaderRead); + + sourceStage = vk::PipelineStageFlagBits::eTransfer; + destinationStage = vk::PipelineStageFlagBits::eFragmentShader; + } else { + throw std::invalid_argument("Unsupported layout transition!"); + } + + commandBuffer.pipelineBarrier( + sourceStage, destinationStage, + vk::DependencyFlagBits::eByRegion, + 0, nullptr, + 0, nullptr, + 1, &barrier + ); +} +---- + +===== Semaphores and Fences + +Semaphores and fences are used for coarser-grained synchronization: + +[source,cpp] +---- +// Example of using semaphores and fences for queue synchronization +void RenderFrame(vk::raii::Device& device, vk::Queue graphicsQueue, vk::Queue presentQueue) { + // Wait for the previous frame to finish + vk::Result result = device.waitForFences(1, &*inFlightFence, VK_TRUE, UINT64_MAX); + device.resetFences(1, &*inFlightFence); + + // Acquire the next image from the swapchain + uint32_t imageIndex; + result = device.acquireNextImageKHR(*swapchain, UINT64_MAX, + *imageAvailableSemaphore, nullptr, &imageIndex); + + // Record command buffer + // ... + + // Submit command buffer + vk::SubmitInfo submitInfo; + vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; + submitInfo.setWaitSemaphoreCount(1) + .setPWaitSemaphores(&*imageAvailableSemaphore) + .setPWaitDstStageMask(waitStages) + .setCommandBufferCount(1) + .setPCommandBuffers(&*commandBuffer) + .setSignalSemaphoreCount(1) + .setPSignalSemaphores(&*renderFinishedSemaphore); + + graphicsQueue.submit(1, &submitInfo, *inFlightFence); + + // Present the image + vk::PresentInfoKHR presentInfo; + presentInfo.setWaitSemaphoreCount(1) + .setPWaitSemaphores(&*renderFinishedSemaphore) + .setSwapchainCount(1) + .setPSwapchains(&*swapchain) + .setPImageIndices(&imageIndex); + + result = presentQueue.presentKHR(&presentInfo); +} +---- + +==== How Rendergraphs Help with Synchronization + +Rendergraphs simplify synchronization by: + +1. *Automatic Dependency Tracking*: The rendergraph knows which passes depend on which resources, so it can automatically insert the necessary synchronization primitives. +2. *Optimal Barrier Placement*: The rendergraph can analyze the entire rendering process and place barriers only where needed, reducing overhead. +3. *Layout Transitions*: The rendergraph can automatically handle image layout transitions based on how resources are used. +4. *Resource Aliasing*: The rendergraph can reuse memory for resources that aren't used simultaneously, reducing memory usage. + +===== Example: Implementing a Deferred Renderer with a Rendergraph + +Here's how you might implement a deferred renderer using a rendergraph: + +[source,cpp] +---- +void SetupDeferredRenderer(Rendergraph& graph, uint32_t width, uint32_t height) { + // Add resources + graph.AddResource("GBuffer_Position", vk::Format::eR16G16B16A16Sfloat, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + + graph.AddResource("GBuffer_Normal", vk::Format::eR16G16B16A16Sfloat, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + + graph.AddResource("GBuffer_Albedo", vk::Format::eR8G8B8A8Unorm, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + + graph.AddResource("Depth", vk::Format::eD32Sfloat, {width, height}, + vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eDepthStencilAttachmentOptimal); + + graph.AddResource("FinalColor", vk::Format::eR8G8B8A8Unorm, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal); + + // Add geometry pass + graph.AddPass("GeometryPass", + {}, // No inputs + {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, + [&](vk::raii::CommandBuffer& cmd) { + // Begin dynamic rendering + std::array colorAttachments; + + // Set up color attachments for G-buffer + // ... + + // Set up depth attachment + // ... + + vk::RenderingInfoKHR renderingInfo; + renderingInfo.setRenderArea({{0, 0}, {width, height}}) + .setLayerCount(1) + .setColorAttachmentCount(colorAttachments.size()) + .setPColorAttachments(colorAttachments.data()) + .setPDepthAttachment(&depthAttachment); + + cmd.beginRendering(renderingInfo); + + // Bind pipeline and draw geometry + // ... + + cmd.endRendering(); + }); + + // Add lighting pass + graph.AddPass("LightingPass", + {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, + {"FinalColor"}, + [&](vk::raii::CommandBuffer& cmd) { + // Begin dynamic rendering + vk::RenderingAttachmentInfoKHR colorAttachment; + // Set up color attachment for final color + // ... + + vk::RenderingInfoKHR renderingInfo; + renderingInfo.setRenderArea({{0, 0}, {width, height}}) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment); + + cmd.beginRendering(renderingInfo); + + // Bind pipeline and draw full-screen quad + // ... + + cmd.endRendering(); + }); + + // Compile the rendergraph + graph.Compile(); +} +---- + +==== Best Practices for Rendergraphs and Synchronization + +1. *Minimize Synchronization*: Use the rendergraph to minimize the number of synchronization points. +2. *Batch Similar Operations*: Group similar operations together to reduce state changes. +3. *Use Appropriate Access Flags*: Be specific about which access types you need to synchronize. +4. *Avoid Redundant Barriers*: Let the rendergraph eliminate redundant barriers. +5. *Consider Memory Aliasing*: Use the rendergraph's memory aliasing capabilities to reduce memory usage. +6. *Profile and Optimize*: Use GPU profiling tools to identify synchronization bottlenecks. +7. *Handle Platform Differences*: Different GPUs may have different synchronization requirements. + ==== Benefits of Dynamic Rendering Dynamic rendering offers several advantages over traditional render passes: From b476bd123c50a357672c1d21bfbca069bba8ad2e Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 18 Jul 2025 16:53:31 -0700 Subject: [PATCH 005/102] Add architectural pattern diagrams and refine related sections - Inserted new diagrams for layered architecture, component-based architecture, data-oriented design, and service locator pattern in the engine tutorial. - Adjusted and reordered content in corresponding sections for improved clarity and structure. - Updated references to foundational concepts, ensuring consistency across sections. --- .../02_architectural_patterns.adoc | 16 ++- en/Building_a_Simple_Engine/introduction.adoc | 5 +- .../component_based_architecture_diagram.svg | 101 ++++++++++++++++++ images/data_oriented_design_diagram.svg | 81 ++++++++++++++ images/layered_architecture_diagram.svg | 47 ++++++++ images/service_locator_pattern_diagram.svg | 75 +++++++++++++ 6 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 images/component_based_architecture_diagram.svg create mode 100644 images/data_oriented_design_diagram.svg create mode 100644 images/layered_architecture_diagram.svg create mode 100644 images/service_locator_pattern_diagram.svg diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index b2afa969..6fe9537b 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -17,13 +17,15 @@ In this section, we'll explore common architectural patterns used in modern rend One of the most fundamental architectural patterns is the layered architecture, where the system is divided into distinct layers, each with a specific responsibility. +image::../../../images/layered_architecture_diagram.svg[Layered Architecture Diagram, width=600] + ==== Typical Layers in a Rendering Engine -1. *Application Layer* - Handles user input, game logic, and high-level application flow. -2. *Scene Management Layer* - Manages the scene graph, spatial partitioning, and culling. +1. *Platform Abstraction Layer* - Provides a consistent interface to platform-specific functionality. +2. *Resource Management Layer* - Manages loading, caching, and unloading of assets. 3. *Rendering Layer* - Handles the rendering pipeline, shaders, and graphics API interaction. -4. *Resource Management Layer* - Manages loading, caching, and unloading of assets. -5. *Platform Abstraction Layer* - Provides a consistent interface to platform-specific functionality. +4. *Scene Management Layer* - Manages the scene graph, spatial partitioning, and culling. +5. *Application Layer* - Handles user input, game logic, and high-level application flow. ==== Benefits of Layered Architecture @@ -96,6 +98,8 @@ public: Component-based architecture is widely used in modern game engines. It promotes composition over inheritance and allows for more flexible entity design. +image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600] + ==== Key Concepts 1. *Entities* - Basic containers that represent objects in the game world. @@ -177,6 +181,8 @@ public: Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. +image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=600] + ==== Key Concepts 1. *Data Layout* - Organizing data for cache-friendly access patterns. @@ -243,6 +249,8 @@ public: The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. +image::../../../images/service_locator_pattern_diagram.svg[Service Locator Pattern Diagram, width=600] + ==== Key Concepts 1. *Service Interface* - Defines the contract for a service. diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc index dc43e615..9104295d 100644 --- a/en/Building_a_Simple_Engine/introduction.adoc +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -17,7 +17,10 @@ Welcome to the "Building a Simple Engine" tutorial series! This series marks a t While the previous tutorial series focused on introducing individual Vulkan concepts step by step, this series takes a different approach: -* *Intermediate to Advanced Level* - This tutorial assumes you have completed the original Vulkan tutorial series and are comfortable with the fundamental concepts. If this feels too advanced, start with the foundational tutorial and come back when you're ready. +* *Intermediate to Advanced Level* - This tutorial assumes you have completed + the original link:../00_Introduction.adoc[Vulkan Tutorial] series and are + comfortable with the + fundamental concepts. If this feels too advanced, start with the link:../00_Introduction.adoc[Vulkan Tutorial] and come back when you're ready. * *Concept-Focused Learning* - Rather than providing exhaustive details on every possible implementation approach, we focus on key architectural concepts and design patterns for building a rendering engine. diff --git a/images/component_based_architecture_diagram.svg b/images/component_based_architecture_diagram.svg new file mode 100644 index 00000000..b052c758 --- /dev/null +++ b/images/component_based_architecture_diagram.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + Component-Based Architecture + + + + Entity 1 + + + + Transform + + + Mesh + + + Physics + + + + Entity 2 + + + + Transform + + + Audio + + + AI + + + + Entity 3 + + + + Transform + + + Mesh + + + Light + + + + Render System + + + Physics System + + + Audio System + + + + + + + + + + + Systems process entities with specific components + Components define entity behavior and data + Entities are just containers for components + + + + + + + + + + + + + + diff --git a/images/data_oriented_design_diagram.svg b/images/data_oriented_design_diagram.svg new file mode 100644 index 00000000..d0611425 --- /dev/null +++ b/images/data_oriented_design_diagram.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + Data-Oriented Design + + + Object-Oriented Approach + + + + GameObject 1 + position: (10, 20, 30) + rotation: (0, 45, 0) + scale: (1, 1, 1) + + + GameObject 2 + position: (50, 0, 10) + rotation: (90, 0, 0) + scale: (2, 2, 2) + + + GameObject 3 + position: (0, 10, 50) + rotation: (0, 180, 0) + scale: (1, 3, 1) + + + VS + + + Data-Oriented Approach + + + + Positions Array + [10,20,30, 50,0,10, 0,10,50, ...] + + + Rotations Array + [0,45,0, 90,0,0, 0,180,0, ...] + + + Scales Array + [1,1,1, 2,2,2, 1,3,1, ...] + + + + Transform System + + + Physics System + + + + + + + + Objects with mixed data types + Contiguous arrays of same data type + diff --git a/images/layered_architecture_diagram.svg b/images/layered_architecture_diagram.svg new file mode 100644 index 00000000..5ba206ec --- /dev/null +++ b/images/layered_architecture_diagram.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + Layered Architecture + + + + Application Layer + + + Scene Management Layer + + + Rendering Layer + + + Resource Management Layer + + + Platform Abstraction Layer + + + + + + + + + + + + + diff --git a/images/service_locator_pattern_diagram.svg b/images/service_locator_pattern_diagram.svg new file mode 100644 index 00000000..29ced464 --- /dev/null +++ b/images/service_locator_pattern_diagram.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + Service Locator Pattern + + + + IAudioService + + PlaySound(soundName) + + + + OpenALAudioService + implements IAudioService + + + NullAudioService + implements IAudioService + + + ... + Other services + + + + ServiceLocator + + GetAudioService() + + ProvideAudioService(service) + + + + Client Code + + + + + + + + + + + + + + + + + + + Concrete Implementations + Service Interface + Global Access Point + Uses services via locator + From f1d10cf48ef6c1136f4b673558fc25184a5fe3e8 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 18 Jul 2025 16:59:51 -0700 Subject: [PATCH 006/102] Add explanatory note on windowing libraries in input handling tutorial - Included a detailed clarification about windowing libraries and their purpose. - Provided examples of popular windowing libraries like GLFW, SDL, Qt, and SFML. - Enhanced understanding of platform-specific abstraction benefits for developers. --- en/Building_a_Simple_Engine/GUI/03_input_handling.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc index 0eaa3ce6..db4243f7 100644 --- a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc +++ b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc @@ -15,6 +15,11 @@ One of the challenges when integrating a GUI into a 3D application is managing i In this section, we'll explore how to handle input for both the GUI and the 3D scene, ensuring a smooth user experience regardless of the windowing system you choose to use. +[NOTE] +==== +A *windowing library* is a software framework that provides functionality for creating and managing application windows, handling user input events (keyboard, mouse, touch), and interfacing with the operating system's display and input systems. Examples include GLFW, SDL, Qt, and SFML. These libraries abstract the platform-specific details of window management and input handling, allowing developers to write code that works across different operating systems without dealing with platform-specific APIs directly. +==== + === Creating a Platform-Agnostic Input System To create an effective input system that works with any windowing library, we need to abstract the input mechanisms and provide a clean interface. Let's define a simple input system that can be adapted to different platforms: From 2a350b2556b831ad2651c858a215b3320d63e827 Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 19 Jul 2025 21:42:46 -0700 Subject: [PATCH 007/102] closer to getting engine working. --- attachments/simple_engine/CMakeLists.txt | 3 +- attachments/simple_engine/audio_system.cpp | 14 +- attachments/simple_engine/audio_system.h | 2 +- attachments/simple_engine/engine.cpp | 2 + attachments/simple_engine/imgui_system.cpp | 8 +- attachments/simple_engine/pipeline.cpp | 139 ++++++++++++++++-- attachments/simple_engine/pipeline.h | 15 +- attachments/simple_engine/renderer.h | 17 ++- .../simple_engine/renderer_compute.cpp | 15 +- attachments/simple_engine/renderer_core.cpp | 25 +++- .../simple_engine/renderer_pipelines.cpp | 132 ++++++++++++++++- .../simple_engine/renderer_resources.cpp | 137 +++++++++++++---- attachments/simple_engine/shaders/hrtf.slang | 4 +- attachments/simple_engine/shaders/imgui.slang | 5 +- .../simple_engine/shaders/lighting.slang | 5 +- attachments/simple_engine/shaders/pbr.slang | 25 ++-- .../simple_engine/shaders/physics.slang | 5 +- .../simple_engine/shaders/texturedMesh.slang | 5 +- .../simple_engine/transform_component.h | 1 - attachments/simple_engine/vulkan_device.cpp | 1 - attachments/simple_engine/vulkan_device.h | 8 +- attachments/simple_engine/vulkan_dispatch.cpp | 7 +- 22 files changed, 454 insertions(+), 121 deletions(-) diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 56c86f6b..c1972eae 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -12,7 +12,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../CMake") find_package (glfw3 REQUIRED) find_package (glm REQUIRED) find_package (Vulkan REQUIRED) -find_package (TinyGLTF REQUIRED) +find_package (tinygltf REQUIRED) find_package (KTX REQUIRED) find_package (stb REQUIRED) @@ -107,7 +107,6 @@ set(SOURCES imgui_system.cpp imgui/imgui.cpp imgui/imgui_draw.cpp - vulkan_dispatch.cpp vulkan_device.cpp pipeline.cpp descriptor_manager.cpp diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 97c8c409..54d335d0 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -9,7 +9,7 @@ // Concrete implementation of AudioSource class ConcreteAudioSource : public AudioSource { public: - ConcreteAudioSource(const std::string& name) : name(name) {} + explicit ConcreteAudioSource(const std::string& name) : name(name) {} ~ConcreteAudioSource() override = default; void Play() override { @@ -51,7 +51,7 @@ class ConcreteAudioSource : public AudioSource { std::cout << "Setting velocity of audio source " << name << " to (" << x << ", " << y << ", " << z << ")" << std::endl; } - bool IsPlaying() const override { + [[nodiscard]] bool IsPlaying() const override { return playing; } @@ -64,10 +64,6 @@ class ConcreteAudioSource : public AudioSource { float velocity[3] = {0.0f, 0.0f, 0.0f}; }; -AudioSystem::AudioSystem() { - // Constructor implementation -} - AudioSystem::~AudioSystem() { // Destructor implementation sources.clear(); @@ -337,12 +333,6 @@ bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { } const vk::raii::Device& device = renderer->GetRaiiDevice(); - // Check if device is valid (using operator bool() instead of ! operator) - if (*device == nullptr) { - std::cerr << "AudioSystem::createHRTFBuffers: Vulkan device is null" << std::endl; - return false; - } - try { // Create input buffer (mono audio) vk::BufferCreateInfo inputBufferInfo; diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index 6d3f38b9..437deb54 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -87,7 +87,7 @@ class AudioSystem { /** * @brief Default constructor. */ - AudioSystem(); + AudioSystem() = default; /** * @brief Destructor for proper cleanup. diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index e70a7d59..39cb6f2c 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -74,6 +74,7 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool } // Initialize physics system + physicsSystem->SetRenderer(renderer.get()); if (!physicsSystem->Initialize()) { return false; } @@ -354,6 +355,7 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo } // Initialize physics system + physicsSystem->SetRenderer(renderer.get()); if (!physicsSystem->Initialize()) { return false; } diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 063d50c9..b0039a14 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -612,7 +612,7 @@ bool ImGuiSystem::createPipeline() { // Create the graphics pipeline with dynamic rendering vk::PipelineRenderingCreateInfo renderingInfo; renderingInfo.colorAttachmentCount = 1; - vk::Format colorFormat = vk::Format::eR8G8B8A8Unorm; // Use the format of your swapchain images + vk::Format colorFormat = renderer->GetSwapChainImageFormat(); // Get the actual swapchain format renderingInfo.pColorAttachmentFormats = &colorFormat; vk::GraphicsPipelineCreateInfo pipelineInfo; @@ -631,11 +631,7 @@ bool ImGuiSystem::createPipeline() { pipelineInfo.basePipelineHandle = nullptr; const vk::raii::Device& device = renderer->GetRaiiDevice(); - auto pipelines = device.createGraphicsPipelines(nullptr, {pipelineInfo}); - pipeline = std::move(pipelines[0]); - - // Shader modules will be automatically cleaned up by RAII - + pipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); return true; } catch (const std::exception& e) { std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; diff --git a/attachments/simple_engine/pipeline.cpp b/attachments/simple_engine/pipeline.cpp index 0bf5b709..abeaca1f 100644 --- a/attachments/simple_engine/pipeline.cpp +++ b/attachments/simple_engine/pipeline.cpp @@ -48,6 +48,76 @@ bool Pipeline::createDescriptorSetLayout() { } } +// Create PBR descriptor set layout +bool Pipeline::createPBRDescriptorSetLayout() { + try { + // Create descriptor set layout bindings for PBR shader + std::array bindings = { + // Binding 0: Uniform buffer (UBO) + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 1: Base color map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 2: Metallic roughness map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 3: Normal map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 4: Occlusion map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 4, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 5: Emissive map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 5, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + } + }; + + // Create descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + pbrDescriptorSetLayout = vk::raii::DescriptorSetLayout(device.getDevice(), layoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR descriptor set layout: " << e.what() << std::endl; + return false; + } +} + // Create graphics pipeline bool Pipeline::createGraphicsPipeline() { try { @@ -232,6 +302,11 @@ bool Pipeline::createGraphicsPipeline() { // Create PBR pipeline bool Pipeline::createPBRPipeline() { try { + // Create PBR descriptor set layout + if (!createPBRDescriptorSetLayout()) { + return false; + } + // Read shader code auto vertShaderCode = readFile("shaders/pbr.spv"); auto fragShaderCode = readFile("shaders/pbr.spv"); @@ -250,17 +325,56 @@ bool Pipeline::createPBRPipeline() { vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, .module = *fragShaderModule, - .pName = "FSMain" + .pName = "PSMain" // Changed from FSMain to PSMain to match the shader }; vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + // Define vertex binding description + vk::VertexInputBindingDescription bindingDescription{ + .binding = 0, + .stride = sizeof(float) * (3 + 3 + 2 + 4), // Position(3) + Normal(3) + UV(2) + Tangent(4) + .inputRate = vk::VertexInputRate::eVertex + }; + + // Define vertex attribute descriptions + std::array attributeDescriptions = { + // Position attribute + vk::VertexInputAttributeDescription{ + .location = 0, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = 0 + }, + // Normal attribute + vk::VertexInputAttributeDescription{ + .location = 1, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = sizeof(float) * 3 + }, + // UV attribute + vk::VertexInputAttributeDescription{ + .location = 2, + .binding = 0, + .format = vk::Format::eR32G32Sfloat, + .offset = sizeof(float) * 6 + }, + // Tangent attribute + vk::VertexInputAttributeDescription{ + .location = 3, + .binding = 0, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = sizeof(float) * 8 + } + }; + // Create vertex input info vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = 0, - .pVertexBindingDescriptions = nullptr, - .vertexAttributeDescriptionCount = 0, - .pVertexAttributeDescriptions = nullptr + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), + .pVertexAttributeDescriptions = attributeDescriptions.data() }; // Create input assembly info @@ -368,10 +482,10 @@ bool Pipeline::createPBRPipeline() { .size = sizeof(MaterialProperties) }; - // Create pipeline layout + // Create pipeline layout using the PBR descriptor set layout vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, - .pSetLayouts = &*descriptorSetLayout, + .pSetLayouts = &*pbrDescriptorSetLayout, // Use PBR descriptor set layout .pushConstantRangeCount = 1, .pPushConstantRanges = &pushConstantRange }; @@ -549,12 +663,19 @@ bool Pipeline::createLightingPipeline() { .pDynamicStates = dynamicStates.data() }; + // Create push constant range for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(MaterialProperties) + }; + // Create pipeline layout vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, .pSetLayouts = &*descriptorSetLayout, - .pushConstantRangeCount = 0, - .pPushConstantRanges = nullptr + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange }; lightingPipelineLayout = vk::raii::PipelineLayout(device.getDevice(), pipelineLayoutInfo); diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h index 599fd392..20691cef 100644 --- a/attachments/simple_engine/pipeline.h +++ b/attachments/simple_engine/pipeline.h @@ -48,6 +48,12 @@ class Pipeline { */ bool createDescriptorSetLayout(); + /** + * @brief Create the PBR descriptor set layout. + * @return True if the PBR descriptor set layout was created successfully, false otherwise. + */ + bool createPBRDescriptorSetLayout(); + /** * @brief Create the graphics pipeline. * @return True if the graphics pipeline was created successfully, false otherwise. @@ -133,6 +139,12 @@ class Pipeline { */ vk::raii::DescriptorSetLayout& getComputeDescriptorSetLayout() { return computeDescriptorSetLayout; } + /** + * @brief Get the PBR descriptor set layout. + * @return The PBR descriptor set layout. + */ + vk::raii::DescriptorSetLayout& getPBRDescriptorSetLayout() { return pbrDescriptorSetLayout; } + private: // Vulkan device VulkanDevice& device; @@ -153,8 +165,9 @@ class Pipeline { vk::raii::Pipeline computePipeline = nullptr; vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; - // Descriptor set layout + // Descriptor set layouts vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; // Helper functions vk::raii::ShaderModule createShaderModule(const std::vector& code); diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 20c9ec82..cc461910 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -5,12 +5,11 @@ #else import vulkan_hpp; #endif +#include #include #include #include -#include #include -#include #include #include "platform.h" @@ -204,6 +203,14 @@ class Renderer { return *commandBuffers[currentFrame]; } + /** + * @brief Get the swap chain image format. + * @return The swap chain image format. + */ + vk::Format GetSwapChainImageFormat() const { + return swapChainImageFormat; + } + private: // Platform Platform* platform = nullptr; @@ -270,6 +277,7 @@ class Renderer { // Descriptor set layout and pool vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; vk::raii::DescriptorPool descriptorPool = nullptr; // Mesh resources @@ -291,6 +299,9 @@ class Renderer { }; std::unordered_map textureResources; + // Default texture resources (used when no texture is provided) + TextureResources defaultTextureResources; + // Entity resources struct EntityResources { std::vector uniformBuffers; @@ -347,6 +358,7 @@ class Renderer { bool createImageViews(); bool setupDynamicRendering(); bool createDescriptorSetLayout(); + bool createPBRDescriptorSetLayout(); bool createGraphicsPipeline(); bool createPBRPipeline(); bool createLightingPipeline(); @@ -357,6 +369,7 @@ class Renderer { bool createTextureImage(const std::string& texturePath, TextureResources& resources); bool createTextureImageView(TextureResources& resources); bool createTextureSampler(TextureResources& resources); + bool createDefaultTextureResources(); bool createMeshResources(MeshComponent* meshComponent); bool createUniformBuffers(Entity* entity); bool createDescriptorPool(); diff --git a/attachments/simple_engine/renderer_compute.cpp b/attachments/simple_engine/renderer_compute.cpp index 56035864..ec01f250 100644 --- a/attachments/simple_engine/renderer_compute.cpp +++ b/attachments/simple_engine/renderer_compute.cpp @@ -1,6 +1,5 @@ #include "renderer.h" #include -#include #include #include @@ -10,7 +9,7 @@ bool Renderer::createComputePipeline() { try { // Read compute shader code - auto computeShaderCode = readFile("shaders/compute.comp.spv"); + auto computeShaderCode = readFile("shaders/compute.spv"); // Create shader module vk::raii::ShaderModule computeShaderModule = createShaderModule(computeShaderCode); @@ -47,7 +46,7 @@ bool Renderer::createComputePipeline() { }, vk::DescriptorSetLayoutBinding{ .binding = 3, - .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eCompute, .pImmutableSamplers = nullptr @@ -80,10 +79,14 @@ bool Renderer::createComputePipeline() { computePipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); // Create compute descriptor pool - std::array poolSizes = { + std::array poolSizes = { vk::DescriptorPoolSize{ .type = vk::DescriptorType::eStorageBuffer, - .descriptorCount = 4u * MAX_FRAMES_IN_FLIGHT + .descriptorCount = 3u * MAX_FRAMES_IN_FLIGHT + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1u * MAX_FRAMES_IN_FLIGHT } }; @@ -172,7 +175,7 @@ void Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint3 .dstBinding = 3, .dstArrayElement = 0, .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = ¶msBufferInfo } }; diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index d6e60f7a..decf5fc0 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -1,10 +1,10 @@ #include "renderer.h" #include -#include -#include #include #include -#include +#include + +VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file #ifdef __INTELLISENSE__ #include @@ -61,6 +61,9 @@ Renderer::~Renderer() { // Initialize the renderer bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) { + vk::detail::DynamicLoader dl; + auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); + VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); // Create Vulkan instance if (!createInstance(appName, enableValidationLayers)) { return false; @@ -122,11 +125,11 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // // Create compute pipeline - // if (!createComputePipeline()) { - // std::cerr << "Failed to create compute pipeline" << std::endl; - // return false; - // } + // Create compute pipeline + if (!createComputePipeline()) { + std::cerr << "Failed to create compute pipeline" << std::endl; + return false; + } // Create command pool if (!createCommandPool()) { @@ -143,6 +146,12 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + // Create default texture resources + if (!createDefaultTextureResources()) { + std::cerr << "Failed to create default texture resources" << std::endl; + return false; + } + // Create command buffers if (!createCommandBuffers()) { return false; diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index 3e23fc09..493e7725 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -1,6 +1,5 @@ #include "renderer.h" #include -#include #include #include #include "mesh_component.h" @@ -43,6 +42,76 @@ bool Renderer::createDescriptorSetLayout() { } } +// Create PBR descriptor set layout +bool Renderer::createPBRDescriptorSetLayout() { + try { + // Create descriptor set layout bindings for PBR shader + std::array bindings = { + // Binding 0: Uniform buffer (UBO) + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 1: Base color map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 2: Metallic roughness map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 3: Normal map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 4: Occlusion map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 4, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 5: Emissive map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 5, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + } + }; + + // Create descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + pbrDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR descriptor set layout: " << e.what() << std::endl; + return false; + } +} + // Create graphics pipeline bool Renderer::createGraphicsPipeline() { try { @@ -187,6 +256,11 @@ bool Renderer::createGraphicsPipeline() { // Create PBR pipeline bool Renderer::createPBRPipeline() { try { + // Create PBR descriptor set layout + if (!createPBRDescriptorSetLayout()) { + return false; + } + // Read shader code auto vertShaderCode = readFile("shaders/pbr.spv"); auto fragShaderCode = readFile("shaders/pbr.spv"); @@ -210,9 +284,44 @@ bool Renderer::createPBRPipeline() { vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - // Create vertex input info - auto bindingDescription = Vertex::getBindingDescription(); - auto attributeDescriptions = Vertex::getAttributeDescriptions(); + // Define vertex binding description + vk::VertexInputBindingDescription bindingDescription{ + .binding = 0, + .stride = sizeof(float) * (3 + 3 + 2 + 4), // Position(3) + Normal(3) + UV(2) + Tangent(4) + .inputRate = vk::VertexInputRate::eVertex + }; + + // Define vertex attribute descriptions + std::array attributeDescriptions = { + // Position attribute + vk::VertexInputAttributeDescription{ + .location = 0, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = 0 + }, + // Normal attribute + vk::VertexInputAttributeDescription{ + .location = 1, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = sizeof(float) * 3 + }, + // UV attribute + vk::VertexInputAttributeDescription{ + .location = 2, + .binding = 0, + .format = vk::Format::eR32G32Sfloat, + .offset = sizeof(float) * 6 + }, + // Tangent attribute + vk::VertexInputAttributeDescription{ + .location = 3, + .binding = 0, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = sizeof(float) * 8 + } + }; vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ .vertexBindingDescriptionCount = 1, @@ -291,10 +400,10 @@ bool Renderer::createPBRPipeline() { .size = sizeof(MaterialProperties) }; - // Create pipeline layout + // Create pipeline layout using the PBR descriptor set layout vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, - .pSetLayouts = &*descriptorSetLayout, + .pSetLayouts = &*pbrDescriptorSetLayout, .pushConstantRangeCount = 1, .pPushConstantRanges = &pushConstantRange }; @@ -438,12 +547,19 @@ bool Renderer::createLightingPipeline() { .pDynamicStates = dynamicStates.data() }; + // Create push constant range for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(MaterialProperties) + }; + // Create pipeline layout vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, .pSetLayouts = &*descriptorSetLayout, - .pushConstantRangeCount = 0, - .pPushConstantRanges = nullptr + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange }; lightingPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 5f6f1dcd..23c845a9 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include // Define STB_IMAGE_IMPLEMENTATION before including stb_image.h to provide the implementation #define STB_IMAGE_IMPLEMENTATION @@ -155,6 +155,80 @@ bool Renderer::createTextureImageView(TextureResources& resources) { } } +// Create default texture resources (1x1 white texture) +bool Renderer::createDefaultTextureResources() { + try { + // Create a 1x1 white texture + const uint32_t width = 1; + const uint32_t height = 1; + const uint32_t pixelSize = 4; // RGBA + const std::vector pixels = {255, 255, 255, 255}; // White pixel (RGBA) + + // Create staging buffer + vk::DeviceSize imageSize = width * height * pixelSize; + auto [stagingBuffer, stagingBufferMemory] = createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy pixel data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels.data(), static_cast(imageSize)); + stagingBufferMemory.unmapMemory(); + + // Create texture image + auto [textureImg, textureImgMem] = createImage( + width, + height, + vk::Format::eR8G8B8A8Srgb, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + defaultTextureResources.textureImage = std::move(textureImg); + defaultTextureResources.textureImageMemory = std::move(textureImgMem); + + // Transition image layout for copy + transitionImageLayout( + *defaultTextureResources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal + ); + + // Copy buffer to image + copyBufferToImage( + *stagingBuffer, + *defaultTextureResources.textureImage, + width, + height + ); + + // Transition image layout for shader access + transitionImageLayout( + *defaultTextureResources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eShaderReadOnlyOptimal + ); + + // Create texture image view + defaultTextureResources.textureImageView = createImageView( + defaultTextureResources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageAspectFlagBits::eColor + ); + + // Create texture sampler + return createTextureSampler(defaultTextureResources); + } catch (const std::exception& e) { + std::cerr << "Failed to create default texture resources: " << e.what() << std::endl; + return false; + } +} + // Create texture sampler bool Renderer::createTextureSampler(TextureResources& resources) { try { @@ -281,9 +355,6 @@ bool Renderer::createUniformBuffers(Entity* entity) { // Create entity resources EntityResources resources; - resources.uniformBuffers.reserve(MAX_FRAMES_IN_FLIGHT); - resources.uniformBuffersMemory.reserve(MAX_FRAMES_IN_FLIGHT); - resources.uniformBuffersMapped.reserve(MAX_FRAMES_IN_FLIGHT); // Create uniform buffers vk::DeviceSize bufferSize = sizeof(UniformBufferObject); @@ -294,9 +365,11 @@ bool Renderer::createUniformBuffers(Entity* entity) { vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); - resources.uniformBuffers[i] = std::move(buffer); - resources.uniformBuffersMemory[i] = std::move(bufferMemory); - resources.uniformBuffersMapped[i] = resources.uniformBuffersMemory[i].mapMemory(0, bufferSize); + void* mappedMemory = bufferMemory.mapMemory(0, bufferSize); + + resources.uniformBuffers.emplace_back(std::move(buffer)); + resources.uniformBuffersMemory.emplace_back(std::move(bufferMemory)); + resources.uniformBuffersMapped.emplace_back(mappedMemory); } // Add to entity resources map @@ -383,6 +456,7 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .range = sizeof(UniformBufferObject) }; + // Always update both descriptors std::array descriptorWrites; // Uniform buffer descriptor write @@ -395,12 +469,28 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .pBufferInfo = &bufferInfo }; + // Check if texture resources are valid + bool hasValidTexture = !texturePath.empty() && + *textureRes.textureSampler && + *textureRes.textureImageView; + // Texture sampler descriptor - vk::DescriptorImageInfo imageInfo{ - .sampler = *textureRes.textureSampler, - .imageView = *textureRes.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; + vk::DescriptorImageInfo imageInfo; + if (hasValidTexture) { + // Use provided texture resources + imageInfo = vk::DescriptorImageInfo{ + .sampler = *textureRes.textureSampler, + .imageView = *textureRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + // Use default texture resources + imageInfo = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } // Texture sampler descriptor write descriptorWrites[1] = vk::WriteDescriptorSet{ @@ -412,7 +502,7 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .pImageInfo = &imageInfo }; - // Update descriptor sets + // Update descriptor sets with both descriptors device.updateDescriptorSets(descriptorWrites, {}); } @@ -460,16 +550,15 @@ std::pair Renderer::createBuffer( // Copy buffer void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size) { try { - // Create command buffer + // Create command buffer using RAII vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; - auto commandBuffers = device.allocateCommandBuffers(allocInfo); - vk::CommandBuffer cmdBuffer = commandBuffers[0]; - vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + vk::raii::CommandBuffers commandBuffers(device, allocInfo); + vk::raii::CommandBuffer& commandBuffer = commandBuffers[0]; // Begin command buffer vk::CommandBufferBeginInfo beginInfo{ @@ -575,16 +664,15 @@ vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format // Transition image layout void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { try { - // Create command buffer + // Create command buffer using RAII vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; - auto commandBuffers = device.allocateCommandBuffers(allocInfo); - vk::CommandBuffer cmdBuffer = commandBuffers[0]; - vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + vk::raii::CommandBuffers commandBuffers(device, allocInfo); + vk::raii::CommandBuffer& commandBuffer = commandBuffers[0]; // Begin command buffer vk::CommandBufferBeginInfo beginInfo{ @@ -666,16 +754,15 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima // Copy buffer to image void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { try { - // Create command buffer + // Create command buffer using RAII vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; - auto commandBuffers = device.allocateCommandBuffers(allocInfo); - vk::CommandBuffer cmdBuffer = commandBuffers[0]; - vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + vk::raii::CommandBuffers commandBuffers(device, allocInfo); + vk::raii::CommandBuffer& commandBuffer = commandBuffers[0]; // Begin command buffer vk::CommandBufferBeginInfo beginInfo{ diff --git a/attachments/simple_engine/shaders/hrtf.slang b/attachments/simple_engine/shaders/hrtf.slang index b84fd78b..e55a98d4 100644 --- a/attachments/simple_engine/shaders/hrtf.slang +++ b/attachments/simple_engine/shaders/hrtf.slang @@ -1,9 +1,6 @@ // Compute shader for HRTF (Head-Related Transfer Function) audio processing // This shader processes audio data to create 3D spatial audio effects -// Workgroup size -[[vk::compute_shader_input(local_size_x = 256)]] - // Input/output buffer bindings [[vk::binding(0, 0)]] RWStructuredBuffer inputAudioBuffer; // Raw audio samples [[vk::binding(1, 0)]] RWStructuredBuffer outputAudioBuffer; // Processed audio samples @@ -72,6 +69,7 @@ void CalculateAngles(float3 sourceDir, float3 listenerForward, float3 listenerUp // Main compute shader function [shader("compute")] +[numthreads(64, 1, 1)] void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) { uint index = dispatchThreadID.x; diff --git a/attachments/simple_engine/shaders/imgui.slang b/attachments/simple_engine/shaders/imgui.slang index d554df23..44541432 100644 --- a/attachments/simple_engine/shaders/imgui.slang +++ b/attachments/simple_engine/shaders/imgui.slang @@ -22,8 +22,7 @@ struct PushConstants { // Bindings [[vk::push_constant]] PushConstants pushConstants; -[[vk::binding(0, 0)]] Texture2D fontTexture; -[[vk::binding(0, 0)]] SamplerState fontSampler; +[[vk::binding(0, 0)]] Sampler2D fontTexture; // Vertex shader entry point [[shader("vertex")]] @@ -46,6 +45,6 @@ VSOutput VSMain(VSInput input) float4 PSMain(VSOutput input) : SV_TARGET { // Sample font texture and multiply by color - float4 color = input.Color * fontTexture.Sample(fontSampler, input.UV); + float4 color = input.Color * fontTexture.Sample(input.UV); return color; } diff --git a/attachments/simple_engine/shaders/lighting.slang b/attachments/simple_engine/shaders/lighting.slang index 1565e20f..b72ab400 100644 --- a/attachments/simple_engine/shaders/lighting.slang +++ b/attachments/simple_engine/shaders/lighting.slang @@ -36,8 +36,7 @@ struct PushConstants { // Bindings [[vk::binding(0, 0)]] ConstantBuffer ubo; -[[vk::binding(1, 0)]] Texture2D texSampler; -[[vk::binding(1, 0)]] SamplerState texSamplerSampler; +[[vk::binding(1, 0)]] Sampler2D texSampler; // Push constants [[vk::push_constant]] PushConstants material; @@ -69,7 +68,7 @@ VSOutput VSMain(VSInput input) float4 PSMain(VSOutput input) : SV_TARGET { // Sample texture - float4 texColor = texSampler.Sample(texSamplerSampler, input.TexCoord); + float4 texColor = texSampler.Sample(input.TexCoord); // Normalize vectors float3 normal = normalize(input.Normal); diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 06f50a80..6b2a37b7 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -50,16 +50,11 @@ static const float PI = 3.14159265359; // Bindings [[vk::binding(0, 0)]] ConstantBuffer ubo; -[[vk::binding(1, 0)]] Texture2D baseColorMap; -[[vk::binding(1, 0)]] SamplerState baseColorSampler; -[[vk::binding(2, 0)]] Texture2D metallicRoughnessMap; -[[vk::binding(2, 0)]] SamplerState metallicRoughnessSampler; -[[vk::binding(3, 0)]] Texture2D normalMap; -[[vk::binding(3, 0)]] SamplerState normalSampler; -[[vk::binding(4, 0)]] Texture2D occlusionMap; -[[vk::binding(4, 0)]] SamplerState occlusionSampler; -[[vk::binding(5, 0)]] Texture2D emissiveMap; -[[vk::binding(5, 0)]] SamplerState emissiveSampler; +[[vk::binding(1, 0)]] Sampler2D baseColorMap; +[[vk::binding(2, 0)]] Sampler2D metallicRoughnessMap; +[[vk::binding(3, 0)]] Sampler2D normalMap; +[[vk::binding(4, 0)]] Sampler2D occlusionMap; +[[vk::binding(5, 0)]] Sampler2D emissiveMap; [[vk::push_constant]] PushConstants material; @@ -120,18 +115,18 @@ VSOutput VSMain(VSInput input) float4 PSMain(VSOutput input) : SV_TARGET { // Sample material textures - float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; - float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float4 baseColor = baseColorMap.Sample(input.UV) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(input.UV).bg; float metallic = metallicRoughness.x * material.metallicFactor; float roughness = metallicRoughness.y * material.roughnessFactor; - float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; - float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; + float ao = occlusionMap.Sample(input.UV).r; + float3 emissive = emissiveMap.Sample(input.UV).rgb; // Calculate normal in tangent space float3 N = normalize(input.Normal); if (material.normalTextureSet >= 0) { // Apply normal mapping - float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + float3 tangentNormal = normalMap.Sample(input.UV).xyz * 2.0 - 1.0; float3 T = normalize(input.Tangent.xyz); float3 B = normalize(cross(N, T)) * input.Tangent.w; float3x3 TBN = float3x3(T, B, N); diff --git a/attachments/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang index 3abf3034..4772c25d 100644 --- a/attachments/simple_engine/shaders/physics.slang +++ b/attachments/simple_engine/shaders/physics.slang @@ -1,8 +1,6 @@ // Compute shader for physics simulation // This shader processes rigid body physics data to simulate physical interactions -// Workgroup size -[[vk::compute_shader_input(local_size_x = 64)]] // Physics data structure struct PhysicsData { @@ -61,6 +59,7 @@ float4 quatNormalize(float4 q) { // Integration shader - updates positions and velocities [shader("compute")] +[numthreads(64, 1, 1)] void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { uint index = dispatchThreadID.x; @@ -190,6 +189,7 @@ void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { // Narrow phase collision detection - detailed collision detection for potential pairs [shader("compute")] +[numthreads(64, 1, 1)] void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { uint index = dispatchThreadID.x; @@ -249,6 +249,7 @@ void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { // Collision resolution - resolves detected collisions [shader("compute")] +[numthreads(64, 1, 1)] void ResolveCS(uint3 dispatchThreadID : SV_DispatchThreadID) { uint index = dispatchThreadID.x; diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang index 3e86d615..70fae67f 100644 --- a/attachments/simple_engine/shaders/texturedMesh.slang +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -24,8 +24,7 @@ struct UniformBufferObject { // Bindings [[vk::binding(0, 0)]] ConstantBuffer ubo; -[[vk::binding(1, 0)]] Texture2D texSampler; -[[vk::binding(1, 0)]] SamplerState texSamplerSampler; +[[vk::binding(1, 0)]] Sampler2D texSampler; // Vertex shader entry point [[shader("vertex")]] @@ -48,5 +47,5 @@ VSOutput VSMain(VSInput input) float4 PSMain(VSOutput input) : SV_TARGET { // Sample texture and multiply by color - return texSampler.Sample(texSamplerSampler, input.TexCoord) * float4(input.Color, 1.0); + return texSampler.Sample(input.TexCoord) * float4(input.Color, 1.0); } diff --git a/attachments/simple_engine/transform_component.h b/attachments/simple_engine/transform_component.h index 1e70a034..fa12050a 100644 --- a/attachments/simple_engine/transform_component.h +++ b/attachments/simple_engine/transform_component.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "component.h" diff --git a/attachments/simple_engine/vulkan_device.cpp b/attachments/simple_engine/vulkan_device.cpp index 2423c299..bd6d9136 100644 --- a/attachments/simple_engine/vulkan_device.cpp +++ b/attachments/simple_engine/vulkan_device.cpp @@ -1,7 +1,6 @@ #include "vulkan_device.h" #include #include -#include #include #include diff --git a/attachments/simple_engine/vulkan_device.h b/attachments/simple_engine/vulkan_device.h index b121a824..ad1b63ce 100644 --- a/attachments/simple_engine/vulkan_device.h +++ b/attachments/simple_engine/vulkan_device.h @@ -1,15 +1,13 @@ #pragma once +#include +#include +#include #ifdef __INTELLISENSE__ #include #else import vulkan_hpp; #endif -#include -#include -#include -#include -#include /** * @brief Structure for Vulkan queue family indices. diff --git a/attachments/simple_engine/vulkan_dispatch.cpp b/attachments/simple_engine/vulkan_dispatch.cpp index f87e64a2..e9f05099 100644 --- a/attachments/simple_engine/vulkan_dispatch.cpp +++ b/attachments/simple_engine/vulkan_dispatch.cpp @@ -3,11 +3,8 @@ #else import vulkan_hpp; #endif -#include // Define the defaultDispatchLoaderDynamic variable -namespace vk { - namespace detail { - vk::DispatchLoaderDynamic defaultDispatchLoaderDynamic; - } +namespace vk::detail { + DispatchLoaderDynamic defaultDispatchLoaderDynamic; } From 2d46280bbe041938b3e4d6ce4a0df9933301c009 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 14:09:02 -0700 Subject: [PATCH 008/102] address feedback --- .../Engine_Architecture/01_introduction.adoc | 2 +- .../03_component_systems.adoc | 7 +- .../component_based_architecture_diagram.svg | 67 ++++++++++++------- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc index 4beb91fb..58a91a9b 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -11,7 +11,7 @@ == Introduction -Welcome to the "Engine Architecture" chapter of our "Building a Simple +Welcome to the "Engine Architecture" chapter of our "Building a Simple Game Engine" series! In this chapter, we'll explore the fundamental architectural patterns and design principles that form the backbone of a modern Vulkan rendering engine. diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc index 9735b535..94170bf2 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc @@ -154,6 +154,8 @@ Let's implement some common component types that you might use in a rendering en [source,cpp] ---- // Transform component +// Handles the position, rotation, and scale of an entity in 3D space +// AffineTransform or "Pose" matrix. class TransformComponent : public Component { private: glm::vec3 position = glm::vec3(0.0f); @@ -199,6 +201,7 @@ public: }; // Mesh component +// Manages the visual representation of an entity by handling its 3D mesh and material class MeshComponent : public Component { private: Mesh* mesh = nullptr; @@ -228,6 +231,7 @@ public: }; // Camera component +// Defines a viewpoint for rendering the scene by managing view and projection matrices class CameraComponent : public Component { private: float fieldOfView = 45.0f; @@ -300,7 +304,7 @@ void MeshComponent::Update(float deltaTime) { ---- This approach is straightforward but creates tight coupling between -components. Tight coupling makes it very difficult to impossible to create +components. Tight coupling makes it challenging or impossible to create unit tests and properly test the engine, so this approach should be avoided in production code. @@ -361,6 +365,7 @@ public: }; // Component that listens for events +// Handles physics-related behavior and responds to collision events through the event system class PhysicsComponent : public Component, public EventListener { public: void Initialize() override { diff --git a/images/component_based_architecture_diagram.svg b/images/component_based_architecture_diagram.svg index b052c758..b4adb518 100644 --- a/images/component_based_architecture_diagram.svg +++ b/images/component_based_architecture_diagram.svg @@ -1,5 +1,5 @@ - + @@ -25,21 +26,21 @@ Entity 1 - - - Transform + + + Transform - - Mesh + + Mesh - - Physics + + Physics Entity 2 - + Transform @@ -53,15 +54,15 @@ Entity 3 - - - Transform + + + Transform - - Mesh + + Mesh - - Light + + Light @@ -74,10 +75,10 @@ Audio System - - + + - + @@ -87,15 +88,31 @@ Entities are just containers for components - - - + + + - - - + + + + + + + Legend + + + + Entity + + + + Component + + + + System From 3b3dc05f9b2ddb4a7ef624c93c42c26f96da0b78 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 14:21:08 -0700 Subject: [PATCH 009/102] address feedback --- .../Engine_Architecture/01_introduction.adoc | 9 +- .../02_architectural_patterns.adoc | 239 ++++++++++++------ .../03_component_systems.adoc | 2 +- 3 files changed, 163 insertions(+), 87 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc index 58a91a9b..e2e0d711 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -11,11 +11,16 @@ == Introduction -Welcome to the "Engine Architecture" chapter of our "Building a Simple Game -Engine" series! In this chapter, we'll explore the fundamental architectural +Welcome to the "Engine Architecture" chapter of our "Building a Simple Engine" +series! In this chapter, we'll explore the fundamental architectural patterns and design principles that form the backbone of a modern Vulkan rendering engine. +While this series focuses primarily on building a rendering engine with Vulkan, +the architectural concepts we'll discuss are applicable to both rendering engines +and full game engines. We'll clarify which patterns are particularly well-suited +for rendering-focused systems versus more general game engine development. + We'll start by taking a step back and considering the overall structure of our engine. A well-designed architecture is crucial for creating a flexible, maintainable, and extensible rendering system. diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index 6fe9537b..4ddc21fe 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -11,7 +11,9 @@ == Architectural Patterns -In this section, we'll explore common architectural patterns used in modern rendering engines. Understanding these patterns will help you make informed decisions when designing your own engine architecture. +In this section, we'll explore and compare common architectural patterns used in modern rendering and game engines. Understanding these patterns, their strengths, weaknesses, and appropriate use cases will help you make informed decisions when designing your own engine architecture. + +Before diving into specific patterns, it's important to clarify that while we're building a Vulkan-based rendering engine in this tutorial, many of the architectural patterns we'll discuss are commonly used in both rendering engines and full game engines. A rendering engine focuses primarily on graphics rendering capabilities, while a full game engine typically includes additional systems like physics, audio, AI, and gameplay logic. === Layered Architecture @@ -94,89 +96,6 @@ public: }; ---- -=== Component-Based Architecture - -Component-based architecture is widely used in modern game engines. It promotes composition over inheritance and allows for more flexible entity design. - -image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600] - -==== Key Concepts - -1. *Entities* - Basic containers that represent objects in the game world. -2. *Components* - Modular pieces of functionality that can be attached to entities. -3. *Systems* - Process entities with specific components to implement game logic. - -==== Benefits of Component-Based Architecture - -* Highly modular and flexible -* Avoids deep inheritance hierarchies -* Enables data-oriented design -* Facilitates parallel processing - -==== Implementation Example - -[source,cpp] ----- -// Component base class -class Component { -public: - virtual ~Component() = default; - virtual void Update(float deltaTime) {} -}; - -// Specific component types -class TransformComponent : public Component { -private: - glm::vec3 position; - glm::quat rotation; - glm::vec3 scale; - -public: - // Methods to manipulate transform -}; - -class MeshComponent : public Component { -private: - Mesh* mesh; - Material* material; - -public: - // Methods to render the mesh -}; - -// Entity class -class Entity { -private: - std::vector> components; - -public: - template - T* AddComponent(Args&&... args) { - static_assert(std::is_base_of::value, "T must derive from Component"); - auto component = std::make_unique(std::forward(args)...); - T* componentPtr = component.get(); - components.push_back(std::move(component)); - return componentPtr; - } - - template - T* GetComponent() { - for (auto& component : components) { - if (T* result = dynamic_cast(component.get())) { - return result; - } - } - return nullptr; - } - - void Update(float deltaTime) { - for (auto& component : components) { - component->Update(deltaTime); - } - } -}; ----- - === Data-Oriented Design Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. @@ -317,6 +236,158 @@ void PlayGameSound() { } ---- +=== Component-Based Architecture + +Component-based architecture is widely used in modern game engines. It promotes composition over inheritance and allows for more flexible entity design. + +image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600] + +==== Key Concepts + +1. *Entities* - Basic containers that represent objects in the game world. +2. *Components* - Modular pieces of functionality that can be attached to entities. +3. *Systems* - Process entities with specific components to implement game logic. + +==== Benefits of Component-Based Architecture + +* Highly modular and flexible +* Avoids deep inheritance hierarchies +* Enables data-oriented design +* Facilitates parallel processing + +==== Implementation Example + +[source,cpp] +---- +// Component base class +class Component { +public: + virtual ~Component() = default; + virtual void Update(float deltaTime) {} +}; + +// Specific component types +class TransformComponent : public Component { +private: + glm::vec3 position; + glm::quat rotation; + glm::vec3 scale; + +public: + // Methods to manipulate transform +}; + +class MeshComponent : public Component { +private: + Mesh* mesh; + Material* material; + +public: + // Methods to render the mesh +}; + +// Entity class +class Entity { +private: + std::vector> components; + +public: + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + for (auto& component : components) { + if (T* result = dynamic_cast(component.get())) { + return result; + } + } + return nullptr; + } + + void Update(float deltaTime) { + for (auto& component : components) { + component->Update(deltaTime); + } + } +}; +---- + +=== Comparative Analysis of Architectural Patterns + +Now that we've explored several architectural patterns, let's compare them directly to understand their relative strengths and weaknesses: + +|=== +| Pattern | Strengths | Weaknesses | Best Used For + +| Layered Architecture +| * Clear separation of concerns + * Easy to understand + * Good for beginners +| * Can lead to "layer bloat" + * May introduce unnecessary indirection + * Potential performance overhead from layer traversal +| * Smaller engines + * Educational projects + * When clarity is more important than performance + +| Component-Based Architecture +| * Highly flexible and modular + * Promotes code reuse + * Avoids deep inheritance hierarchies + * Easier to extend with new features +| * More complex to implement initially + * Can be harder to debug + * Potential performance overhead from component lookups +| * Modern rendering engines + * Systems with diverse object types + * Projects requiring frequent extension + +| Data-Oriented Design +| * Excellent performance + * Cache-friendly memory access + * Good for parallel processing +| * Less intuitive than OOP + * Steeper learning curve + * Can make code harder to read +| * Performance-critical systems + * Mobile platforms + * Systems processing large amounts of similar data + +| Service Locator Pattern +| * Decouples service providers from consumers + * Facilitates testing + * Allows runtime service swapping +| * Can hide dependencies + * Potential for runtime errors + * Global state concerns +| * Cross-cutting concerns + * Systems requiring runtime configuration + * When loose coupling is critical +|=== + +=== Why We're Focusing on Component Systems + +For our Vulkan rendering engine, we've chosen to focus on component-based architecture for several key reasons: + +1. *Flexibility for Graphics Features*: Component systems allow us to easily add, remove, or swap rendering features (like different shading models, post-processing effects, or lighting techniques) without major refactoring. + +2. *Separation of Rendering Concerns*: Components naturally separate different aspects of rendering (geometry, materials, lighting, cameras) into manageable, reusable pieces. + +3. *Industry Standard*: Most modern rendering engines and graphics frameworks use component-based approaches because they provide the right balance of flexibility, maintainability, and performance. + +4. *Extensibility*: As graphics technology evolves rapidly, component systems make it easier to incorporate new Vulkan features or rendering techniques. + +5. *Compatibility with Data-Oriented Optimizations*: While we're using a component-based approach, we can still apply data-oriented design principles within our components for performance-critical rendering paths. + +While other architectural patterns have their merits, component-based architecture provides the best foundation for a modern, flexible rendering engine. That said, we'll incorporate aspects of other patterns where appropriate - using layered architecture for our overall engine structure, data-oriented design for performance-critical systems, and service locators for cross-cutting concerns. + === Conclusion These architectural patterns provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc index 94170bf2..a1dea87f 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc @@ -11,7 +11,7 @@ == Component Systems -In the previous section, we introduced the concept of component-based architecture. Now, let's dive deeper into how to implement effective component systems in your rendering engine. +In the previous section, we introduced several architectural patterns and explained why we're focusing on component-based architecture for our Vulkan rendering engine. As we established, component systems provide the ideal balance of flexibility, modularity, and performance for modern rendering engines. Now, let's dive deeper into how to implement effective component systems in your rendering engine. === The Problem with Deep Inheritance From 6cf03fcf956dc2b14936a98cd913ed3cc0959738 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 14:22:08 -0700 Subject: [PATCH 010/102] address feedback --- .../Engine_Architecture/01_introduction.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc index e2e0d711..9ac98c9e 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -11,8 +11,8 @@ == Introduction -Welcome to the "Engine Architecture" chapter of our "Building a Simple Engine" -series! In this chapter, we'll explore the fundamental architectural +Welcome to the "Engine Architecture" chapter of our "Building a Simple Game +Engine" series! In this chapter, we'll explore the fundamental architectural patterns and design principles that form the backbone of a modern Vulkan rendering engine. From 50d78a70e747bcabbbdd02727e22004186709069 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 14:25:50 -0700 Subject: [PATCH 011/102] add the flowchart to the rendering pipeline. --- .../05_rendering_pipeline.adoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 90eb2ecf..56d055d7 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -13,6 +13,22 @@ A well-designed rendering pipeline is essential for creating a flexible and efficient rendering engine. In this section, we'll explore how to structure your rendering pipeline to support various rendering techniques and effects. +=== Rendering Pipeline Overview + +The following diagram provides a high-level overview of a modern Vulkan rendering pipeline: + +image::../../../images/rendering_pipeline_flowchart.svg[Rendering Pipeline Flowchart, width=600] + +The rendering pipeline consists of several key stages: + +1. *Scene Culling* - Determine which objects are visible and need to be rendered. +2. *Render Pass Management* - Organize rendering into passes with specific purposes. +3. *Command Generation* - Generate commands for the GPU to execute. +4. *Execution* - Submit commands to the GPU for execution. +5. *Post-Processing* - Apply effects to the rendered image. + +Supporting components like Rendergraphs help manage dependencies between render passes, while Synchronization primitives ensure correct execution order. Different rendering techniques (Deferred, Forward+, PBR) can be implemented within this pipeline architecture. + === Rendering Pipeline Challenges When designing a rendering pipeline, you'll need to address several challenges: From 9b2c78146c65a14af16eb2760d594de16aeac98b Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 14:42:08 -0700 Subject: [PATCH 012/102] describe Rendergraphs and Dynamic Rendering --- .../05_rendering_pipeline.adoc | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 56d055d7..9a799490 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -554,9 +554,29 @@ Rendergraphs simplify synchronization by: 3. *Layout Transitions*: The rendergraph can automatically handle image layout transitions based on how resources are used. 4. *Resource Aliasing*: The rendergraph can reuse memory for resources that aren't used simultaneously, reducing memory usage. -===== Example: Implementing a Deferred Renderer with a Rendergraph +==== Dynamic Rendering and Its Integration with Rendergraphs -Here's how you might implement a deferred renderer using a rendergraph: +Dynamic rendering is a modern Vulkan feature that simplifies the rendering process and works particularly well with rendergraphs. Before diving into implementation examples, let's understand what dynamic rendering is and how it relates to our rendering pipeline architecture. + +===== Benefits of Dynamic Rendering + +Dynamic rendering offers several advantages over traditional render passes: + +1. *Simplified API*: No need to create and manage VkRenderPass and VkFramebuffer objects, reducing code complexity. +2. *More Flexible Rendering*: Easier to change render targets and attachment formats at runtime. +3. *Improved Compatibility*: Works better with modern rendering techniques that don't fit well into the traditional render pass model. +4. *Reduced State Management*: Fewer objects to track and synchronize. +5. *Easier Debugging*: Simpler rendering code is easier to debug and maintain. + +With dynamic rendering, we specify all rendering state (render targets, load/store operations, etc.) directly in the vkCmdBeginRendering call, rather than setting it up ahead of time in a VkRenderPass object. This allows for more dynamic rendering workflows and simplifies the implementation of techniques like deferred rendering. + +===== Dynamic Rendering in Rendergraphs + +When combined with rendergraphs, dynamic rendering becomes even more powerful. The rendergraph handles the resource dependencies and synchronization, while dynamic rendering simplifies the actual rendering process. This combination provides both flexibility and performance. + +===== Example: Implementing a Deferred Renderer with a Rendergraph and Dynamic Rendering + +Here's how you might implement a deferred renderer using a rendergraph with dynamic rendering: [source,cpp] ---- @@ -650,18 +670,6 @@ void SetupDeferredRenderer(Rendergraph& graph, uint32_t width, uint32_t height) 6. *Profile and Optimize*: Use GPU profiling tools to identify synchronization bottlenecks. 7. *Handle Platform Differences*: Different GPUs may have different synchronization requirements. -==== Benefits of Dynamic Rendering - -Dynamic rendering offers several advantages over traditional render passes: - -1. *Simplified API*: No need to create and manage VkRenderPass and VkFramebuffer objects, reducing code complexity. -2. *More Flexible Rendering*: Easier to change render targets and attachment formats at runtime. -3. *Improved Compatibility*: Works better with modern rendering techniques that don't fit well into the traditional render pass model. -4. *Reduced State Management*: Fewer objects to track and synchronize. -5. *Easier Debugging*: Simpler rendering code is easier to debug and maintain. - -With dynamic rendering, we specify all rendering state (render targets, load/store operations, etc.) directly in the vkCmdBeginRendering call, rather than setting it up ahead of time in a VkRenderPass object. This allows for more dynamic rendering workflows and simplifies the implementation of techniques like deferred rendering. - [source,cpp] ---- // Forward declarations From ddd11778de3d412153d8b55371aaa957d693abba Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 16:19:05 -0700 Subject: [PATCH 013/102] address comments. --- .../02_architectural_patterns.adoc | 277 ++---------------- images/layered_architecture_diagram.png | Bin 0 -> 14199 bytes images/layered_architecture_diagram.svg | 47 --- images/rendering_pipeline_flowchart.svg | 128 ++++++++ 4 files changed, 148 insertions(+), 304 deletions(-) create mode 100644 images/layered_architecture_diagram.png delete mode 100644 images/layered_architecture_diagram.svg create mode 100644 images/rendering_pipeline_flowchart.svg diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index 4ddc21fe..88ee9a46 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -11,234 +11,57 @@ == Architectural Patterns -In this section, we'll explore and compare common architectural patterns used in modern rendering and game engines. Understanding these patterns, their strengths, weaknesses, and appropriate use cases will help you make informed decisions when designing your own engine architecture. +In this section, we'll provide a quick overview of common architectural patterns used in modern rendering and game engines, with a focus on Component-Based Architecture which forms the foundation of our Vulkan rendering engine. Before diving into specific patterns, it's important to clarify that while we're building a Vulkan-based rendering engine in this tutorial, many of the architectural patterns we'll discuss are commonly used in both rendering engines and full game engines. A rendering engine focuses primarily on graphics rendering capabilities, while a full game engine typically includes additional systems like physics, audio, AI, and gameplay logic. -=== Layered Architecture +=== Overview of Common Architectural Patterns -One of the most fundamental architectural patterns is the layered architecture, where the system is divided into distinct layers, each with a specific responsibility. +Here's a brief introduction to the most common architectural patterns used in game and rendering engines: -image::../../../images/layered_architecture_diagram.svg[Layered Architecture Diagram, width=600] +==== Layered Architecture -==== Typical Layers in a Rendering Engine +Layered architecture divides the system into distinct layers, each with a specific responsibility. Typical layers include platform abstraction, resource management, rendering, scene management, and application layers. -1. *Platform Abstraction Layer* - Provides a consistent interface to platform-specific functionality. -2. *Resource Management Layer* - Manages loading, caching, and unloading of assets. -3. *Rendering Layer* - Handles the rendering pipeline, shaders, and graphics API interaction. -4. *Scene Management Layer* - Manages the scene graph, spatial partitioning, and culling. -5. *Application Layer* - Handles user input, game logic, and high-level application flow. - -==== Benefits of Layered Architecture +image::../../../images/layered_architecture_diagram.png[Layered Architecture +Diagram, width=400] +*Key Benefits:* * Clear separation of concerns * Easier to understand and maintain * Can replace or modify individual layers without affecting others -* Facilitates testing of individual layers - -==== Implementation Example - -[source,cpp] ----- -// Platform Abstraction Layer -class Platform { -public: - virtual void Initialize() = 0; - virtual void* CreateWindow(int width, int height) = 0; - virtual void ProcessEvents() = 0; - // ... -}; - -// Resource Management Layer -class ResourceManager { -public: - virtual Texture* LoadTexture(const std::string& path) = 0; - virtual Mesh* LoadMesh(const std::string& path) = 0; - // ... -}; -// Rendering Layer -class Renderer { -public: - virtual void Initialize(Platform* platform) = 0; - virtual void RenderScene(Scene* scene) = 0; - // ... -}; - -// Scene Management Layer -class SceneManager { -public: - virtual void AddEntity(Entity* entity) = 0; - virtual void UpdateScene(float deltaTime) = 0; - // ... -}; +For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#layered-architecture[Appendix: Layered Architecture]. -// Application Layer -class Application { -private: - Platform* platform; - ResourceManager* resourceManager; - Renderer* renderer; - SceneManager* sceneManager; +==== Data-Oriented Design -public: - void Run() { - platform->Initialize(); - renderer->Initialize(platform); - - // Main loop - while (running) { - platform->ProcessEvents(); - sceneManager->UpdateScene(deltaTime); - renderer->RenderScene(sceneManager->GetActiveScene()); - } - } -}; ----- +Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. It emphasizes cache-friendly memory layouts and bulk processing of data. -=== Data-Oriented Design - -Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. - -image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=600] - -==== Key Concepts - -1. *Data Layout* - Organizing data for cache-friendly access patterns. -2. *Systems* - Process data in bulk, often using SIMD instructions. -3. *Entity-Component-System (ECS)* - A common implementation of DOD principles. - -==== Benefits of Data-Oriented Design +image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=400] +*Key Benefits:* * Better cache utilization * More efficient memory usage * Easier to parallelize -* Can lead to significant performance improvements -==== Implementation Example - -[source,cpp] ----- -// A simple ECS implementation -struct TransformData { - std::vector positions; - std::vector rotations; - std::vector scales; -}; +For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#data-oriented-design[Appendix: Data-Oriented Design]. -struct RenderData { - std::vector meshes; - std::vector materials; -}; - -class TransformSystem { -private: - TransformData& transformData; - -public: - TransformSystem(TransformData& data) : transformData(data) {} - - void Update(float deltaTime) { - // Process all transforms in bulk - for (size_t i = 0; i < transformData.positions.size(); ++i) { - // Update transforms - } - } -}; - -class RenderSystem { -private: - RenderData& renderData; - TransformData& transformData; - -public: - RenderSystem(RenderData& rData, TransformData& tData) - : renderData(rData), transformData(tData) {} - - void Render() { - // Render all entities in bulk - for (size_t i = 0; i < renderData.meshes.size(); ++i) { - // Render mesh with transform - } - } -}; ----- - -=== Service Locator Pattern +==== Service Locator Pattern The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. -image::../../../images/service_locator_pattern_diagram.svg[Service Locator Pattern Diagram, width=600] - -==== Key Concepts - -1. *Service Interface* - Defines the contract for a service. -2. *Service Provider* - Implements the service interface. -3. *Service Locator* - Provides access to services. - -==== Benefits of Service Locator Pattern +image::../../../images/service_locator_pattern_diagram.svg[Service Locator Pattern Diagram, width=400] +*Key Benefits:* * Decouples service consumers from service providers * Allows for easy service replacement * Facilitates testing with mock services -==== Implementation Example - -[source,cpp] ----- -// Audio service interface -class IAudioService { -public: - virtual ~IAudioService() = default; - virtual void PlaySound(const std::string& soundName) = 0; - virtual void StopSound(const std::string& soundName) = 0; -}; - -// Concrete audio service -class OpenALAudioService : public IAudioService { -public: - void PlaySound(const std::string& soundName) override { - // Implementation using OpenAL - } - - void StopSound(const std::string& soundName) override { - // Implementation using OpenAL - } -}; - -// Service locator -class ServiceLocator { -private: - static IAudioService* audioService; - static IAudioService nullAudioService; // Default null service - -public: - static void Initialize() { - audioService = &nullAudioService; - } - - static IAudioService& GetAudioService() { - return *audioService; - } - - static void ProvideAudioService(IAudioService* service) { - if (service == nullptr) { - audioService = &nullAudioService; - } else { - audioService = service; - } - } -}; - -// Usage example -void PlayGameSound() { - ServiceLocator::GetAudioService().PlaySound("explosion"); -} ----- +For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#service-locator-pattern[Appendix: Service Locator Pattern]. === Component-Based Architecture -Component-based architecture is widely used in modern game engines. It promotes composition over inheritance and allows for more flexible entity design. +Component-based architecture is widely used in modern game engines and forms the foundation of our Vulkan rendering engine. It promotes composition over inheritance and allows for more flexible entity design. image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600] @@ -319,59 +142,6 @@ public: }; ---- -=== Comparative Analysis of Architectural Patterns - -Now that we've explored several architectural patterns, let's compare them directly to understand their relative strengths and weaknesses: - -|=== -| Pattern | Strengths | Weaknesses | Best Used For - -| Layered Architecture -| * Clear separation of concerns - * Easy to understand - * Good for beginners -| * Can lead to "layer bloat" - * May introduce unnecessary indirection - * Potential performance overhead from layer traversal -| * Smaller engines - * Educational projects - * When clarity is more important than performance - -| Component-Based Architecture -| * Highly flexible and modular - * Promotes code reuse - * Avoids deep inheritance hierarchies - * Easier to extend with new features -| * More complex to implement initially - * Can be harder to debug - * Potential performance overhead from component lookups -| * Modern rendering engines - * Systems with diverse object types - * Projects requiring frequent extension - -| Data-Oriented Design -| * Excellent performance - * Cache-friendly memory access - * Good for parallel processing -| * Less intuitive than OOP - * Steeper learning curve - * Can make code harder to read -| * Performance-critical systems - * Mobile platforms - * Systems processing large amounts of similar data - -| Service Locator Pattern -| * Decouples service providers from consumers - * Facilitates testing - * Allows runtime service swapping -| * Can hide dependencies - * Potential for runtime errors - * Global state concerns -| * Cross-cutting concerns - * Systems requiring runtime configuration - * When loose coupling is critical -|=== - === Why We're Focusing on Component Systems For our Vulkan rendering engine, we've chosen to focus on component-based architecture for several key reasons: @@ -390,14 +160,7 @@ While other architectural patterns have their merits, component-based architectu === Conclusion -These architectural patterns provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. - -When designing your engine architecture, consider: - -1. *Performance Requirements* - Different patterns have different performance characteristics. -2. *Flexibility Needs* - How much flexibility do you need for future extensions? -3. *Team Size and Experience* - More complex architectures may be harder to work with for smaller teams. -4. *Project Scope* - A small project may not need the complexity of a full ECS. +We've provided a brief overview of common architectural patterns, with a focus on Component-Based Architecture which we'll use throughout this tutorial. For more detailed information about other architectural patterns, including implementation examples and comparative analysis, see the link:../Appendix/appendix.adoc[Appendix: Detailed Architectural Patterns]. In the next section, we'll dive deeper into component systems and how to implement them effectively in your engine. diff --git a/images/layered_architecture_diagram.png b/images/layered_architecture_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c5b0d486601070007fb59b8765067cc05b542c51 GIT binary patch literal 14199 zcmb`OXF!u(m#(7%wxEDYm)=x*5eQACi3mss0TF4^dzFBIbP3X>OYcRH(7PA}0cj$= z1PHy?gmQL#XXbn7n=|LkoH;*;7@nQ%>}T!0u65rlAQZK`RpxMQsjM%`KctNtmh&|3SH2mW5xH-IFv-HFHj)l8%CB`BCIWD^ zigYn~KL6X;4{r0U6G0+ZpIbY=CX#c#rqoBVSH0PHLI-O?MQEI9oaAO%!liyz8)Qe( zUl9H-s@*V~U=c!V9$;b?HKqZ-& zEN>FgladNu@s9`&rVeP4myro?on~grkTd!}UtVg2Y#eFr?M|D)Ec9-UMM#y@idJs7 zMYAuD9;2mV&J+!BINY*}l(0{rq0iyW9prWZ&#>?y-`U|tn#%YKsU{;S9C)H^ultvU zpi9TpW>^k*%JJ-Md0k?4V?%o|acOO113gUdEixedNRu}D@p0BN5lMGl>G}kX z`)b!7@$sy7Q_&}(%m&9LveT$jm+39=0-HkqO+F<(s;Oy$5eM^FxpkrTwV>{;;`%d5@3W;`OPU%sZ2g$v z@LmwR$|g=ls{16hsQ%2`)>(M{qbH5_q>s<^@vas7?poNewwKF6YV9wuvP77)Q;$zF zO5H7L>`J>U*QIEm$3-~~o#9Gx?(_YuoJ}?Fi|vrtFx8SHsP-_5A>_gOgyz)LU~BsC z8vMc@q+7m{>Q}8qTWW4T; zopsF)7*x=M?$6fNNYb7>qYw(cNyZUDC-JrEWajz>-1C#;9bb?3DIe^e)BP&=@j_-q zjwBCXw+noClTmB(EZ*1E8eS0qrnQ;GPM>0VbkNr(U(@V8-#@eQjsEOpNB`#&Js;dA zBb|h2@DzOedwyleatN~|wW1#s;=R)+f2fGnK4v|+_oBg-xY>xk`o!3Lr>EO(Xl0kA zuS(b3iKah2$HliuZM>AzNGw8Z}BXfLB!P#jVf$&s==w9P# z)vwE)q9+)i36x6EyH$}0I!Rg%r^c;?@+*Ek!fX*7p*AOuaOt%jiY9EfWs>tOJ~#wF z9UL5N+4&q^03mm)FYPRgE={mN7dIQG&szfDBE&!2Wyh@oH`rr{M;5`i+M9RK2oGBP zhDen4IgR#YW=lWlsS_UBC_S#PF8Go_=_UAUJE^On;^$OxlhaPSSDdr!M*SOg7p&eW z?eM;^9uM+}k9IFMV#5ViV0_kI>Ye|{MZ^AhHa=*r+I@@ZMUAsX0bIiK@L4QF{FJ^H zX16n2@z78m@qSm9tE$u??P42~8WByNYl&APPfkwKS%=jy!t*Y%PD5km#|!*m3$b}`6&k);3*!kyOsl9J zpr(i|rnH}DKRi2H&J>5!SQ5p-^1tBY&L17bE-g>izoaOht6|!;!>E{o6MC_?4ZZhB{_>ezhDf zBq^mhN`bXi+pyuj=2(}=vI`f|RL5dpu-6_I8(_PZHS8&s2=rqh zd#JOu>pqmv8)>qCw z=hV5wn1a#DNY)Iv&$4qKY#_L9RCv5vpk)f zZ4ax7no{w>W?b-e{3@E4xvI#+NZw}kCRFy?0A~P?SWs5{GMHq{%3@RP8x^i$DT~b3 z>8z5K4XKlxO(%!waiM!R9^i03T_=V-4RL#&5rwAb;sloni46Uh4rFgN1$GBL{_ePv zRQ&n&nUakO!&SOa=F|gQ7Y$4m_6TDsc2Xe|*0m^~U}jkQqArKr0&MDRzd3Mb)(oYh z7sDiXWBuBBDu0@Pm4LT(%tC8SA2gn!BQSg5Xp@NDyy1D|I=XV#&U$z1y6(}@JeJ5C zoTAbcxJA^DptCSS0kF;7vT*B@E8}WYXQu-yL!WnXC(1jXY7%^huQ?mUk3hoFN=cEV zzSDRk{tb2;_dWjmu4Y)!K}kc}%441v^*Y$o0*iw^ut}Si-_(qZ9_9LQOK4}8>KJfy ze&|~=@+qB@@Y|SJuGir^LeoSyorx(<%x;2RBi)sQN{{$8XMVgmlbK&yC>ehT=Wx1| z=43}UckpVE&s2;Nb&Sso!khy--cGs(r69G;(x1(69giRDmUtOfMZ{$epr$iFgWKJ* zA$i22ornb+HUu0xGA=T0Fc%44!v zA)9Nu%zviNlBpmBN4wP*f&=OBcx#7ZhfO$F=d*&0^QgPng_Ms*X)A|eh9}#PodjfL zase%{FGL}oX2Dh?~ZxK`KKUs+gOzIX3lwF6pisGEuDQPULa*Dvy4 zzkdB(+52f(S=QLt6FKF6Ac_)h`SvaFO2g2QW*{Bu(6^%VE(J zomZ<{iJDGgKo~=sJ3FuRq)8dt7zYOjiujz0YiVgg>K7|s3ldEclz%Yw@Ti%MBOwiE z8{*(@OnTp{PJA;1p|yId7&395>q?*$~pawC^zBs5E5BZ zXxoFEvh1$!Sri6O&ov*wDFb~uyOdWc+p0^h4hX93QOfMRZ;`LOAxrvnKzxgV8s$K1 zqDzEwP_|A>#mt}HPu#%WwGYXIK_v@Pc%epP_i0fNDFZcygMxb~iRg!((4rm>+>A4A zDd)_xfNfG_+l&RS)Y=$;3cA1mJb!8 z7!=4T1JgTG+tqT#%_xbU4#bKu81g?+-5h@$4T_kJm=mMzYa#%%cWw7zZmYD zQp&9#2N$$XXFd->>$t3wJ#XtwQN797#c!e;$fEEeXX!yg`H%aVGO7fnk|B0s`!5FM_cMWAK@$4rXwt4@3waei|hWWX#~` z?7mIcp0&S%8fbTbjv>#{2Bu;>mWjdCgVFAby2tY3&ZJ3n?)UG=2GQ`5cJ?PybA!zy8 zch8>kahz!^EJy((TynCVe3~OIhu+?~><2;G6bfH(3}>C&-A~HNmxIe_;=nFpAXB}W zudLmX_gaTxdG1UhDsKa2BVbLClk`O?ip4U9h_*x~_5(+Sr8-Z@vdO!=h@#LnUL^qv zPI=Xw69gIrLYBpq0YOp}-_wB~geDFI>^2Crjs)sfj)pMpma5HTlwNZDKw47P)5WKw za-ErIId*<2Hs%Li<&Hagren{<r+12fNU}}XQ4pNdAMWl(DG6+_*>I!|SWHyuXylxz zoyHJydM2oka^k20XXIVlN$H0kQ3f(^WYQpiX6P(Oi}EyZ>X;>|a&}ScD#!2}IXG*L zdyg+FjXp~*jE2WppjBgSr0m_-1_U*kWvI5kCmVb|DHpNOIxr-=P3RwC`EcP&pvz&o z$fJ#?;F9bbL8Td-r1e-&cS7ul+0EV^N2iy%aryhYaY8?raE(&VEg|ncLo?|ur;Fe zloPgD6_ZgYtHBJbnyVEDQslRd&u^>3QVKByBo|;MsO6Iv2Q8hC=M<=Vr+i;9cSx%Zj(y@obkhNWD4UQF)UKvz4#eP39Z>aC2_=;Y+L z{e9P=?$<@$O%dJQO3rl#yPgtXo^v_FQVNnZ-~4S+kQEjFV^wy>Zf;e}IV3}!{rzN+ zPs1yk<7AeV&#Rp<*A!EtCouFGTa3=^C zt_E-i1w4pvcUMh$>n-zTz+QnT4Vt`fU%h%YyUI{UXFAv}a8`tSk5ctE-(emy=Q4lb z5&^J7-Qxs6P&sj3PRR5BlC$Y6fCsahO+$Ef^= zsF*2vnSJEdq1#~ZbPINWp6_XIm;IO{T1O<5?7cDRfMhHn1Anp`4@P7*j3j6*+~jmq zn*bBtr}Wyh2NkUPNZjjRlwKNd!otF;y^gk<(eXTn+?C7G%HIT8pT>#h_kePWA~1Ya zWHc!Stl87O^qJmN3FAXm8vu{oP#mqY_kGMN?ff_A$sfut?bDexW!2Te_q0p$&J1fD z15A6UV4aKM%#sE7P9-E0X^=K(+(wi3)WGWI25rA?Yg6-!evte>fBx(%g7{ofq3+^D zy)<6qw4B-?sI+x_v|~vlqj6!)6%5QASekAPxT+~^PB(788C&-EU=p1CM=WJ{TKpAF zqie21O8H_$Q!IF1Db}khtE&sk&SottDw_TlPNOMY_!(JMbq-=zza`hs#aQs-f6p4w zMg=Wx?a#Hfq4=>SC9_~wARvP|a;G*j$JR22SJJ`b*qYWv7ObvUGv!9d#r2Y+BHrEL zyf}ay5wWqiAjcmz`6<0_bUccipP&D;;}V~?*l$gkKpUe(MwyvlH2-rE>0i*}Nn(z@ z0Y#uu9uLZaL{@c3>0Y;>5@q1IuJS<1G?oxp6^nw|cx?rv0@-1s)V@a>FUW4I)Utwv zMD*uGpb{XBLckHrGaxuq%iYRb^aVb^t8~L6@!Cn9i34q$V0S{*66cn9gwIa+0=0hx z9mAr)eOS)f6)VHl z)k$c!Qdd%IG5VOT?o9px}xyWY$pcfTWe<=n0~tXQhydw<x{OqAc6JJjgn(QFD)WAa<(v-pgo zHD>=s1>zs!Z8CNrz?#k2Cfw&WPw?<9M<^KAFQ}Pb7>zW=W6)r79)K{3R8y*1 z5jk2^%~9EGCC&ZBeevrLNf@Bg0F@XnP2*0RY5aMino0BKB!P^xB3g_&RXk8>_te~0 z5e2fZ4&uD$ulYd`XofC+7pQ2E2*OC0oU=;}C2{vM^PNImz{MyQ;U|&Tp38ZcA3jWt z-v~=Vtkf2uVcFEf*Yd>yQy(|JnV2C=7X z%&{o=6!&RMTqPx1-zqXE{|bji;rlB=22viy1hvUk^~#TQ5T;|u+F#(pqgta(y^$bO zU3SS_ia^@^VdhJm z+02p37X1a^UtKgF+kb$`cRN%oh*I`UJb9nt_IrDy48fy7CRDHur!QG4P}Zauz1d|# z#>?+GApb*+JLVD**Fa_`Bqb6^qFx&?xF$HX%dP#g^^_2&< zgO?wK+wR?`=tT)Cf%Em5pJ_<6GgOj#y0(ORsof!&X6bP*ONw`7O!BS=O&jlEp1s<| zyS?;oQq5sUd7NExi2b(|ncPb+v#oXFzO=hc=<{~H&aXDu8eK{$x6bT^FFQH3i?&;g zE#kawoBA8erQ)NR&gH~+gk|gB$?ptS)|y+ki91q0DHKs-zl#NU`N^|_Q<3XtB*S?* zw|C`lr>hS-QoefAqEvSR9cUx!OF1w3jdNUY{*4<91St`qDExB6T#lib#Bu1n#&moA zAg{cx(LEd&QpK%*<~||@wzos;>UBi*t*lMc!G3);%Br1)VU5Dpp%>Q7?ZQMr5t1VurkW#zB7bhfdts=w#$=FPB3watDu zm61~Vm?|^!tVefhxXg;%DGu!B&SZ#Ok3?dQ=pGG)B{?{E-aaSQfi?j^j~GK1FC1s7 z$7dh#3v*;YI`PmQ z_R;qawMl}t6QaUcSvLIxX<7MRb;v-)1W_B~oy@#I>ri1E=EZ|E#`+UBI4r4=H`50c<$Bb|3Q6$*JN` z8I7#0B%GE_lg;EiWc$!G-hf1YbJLK~P(1Obx3aQ=yx7$hr1rP-19E{@VZe*hAls@i zfR02AeGwode{N_na1o#`oZ2vUa=I~LOVRM+I*<%flyjHOhUm`d3@`sXK|!^clZ)#d z#IxZ=J6Ae*IWf2OvO&?2lQfM9KaZcbRi7Ih5#iy)Umf13qyQq7(Wr64^vul18){8S z`JC3zk_)NVO$6GEEjq)h?Pnehbthb<-6HLa1Os{{aZt!+%SC#7+mHo9c`pr>l9kz! z3Ul=aGb5vOMwF?_{;!+;FJ5d7+j>3YV`I{ZITcnUeWbv)`PCW2tGke#!hcn# zx+7p20kG37apAWkW_FedBy|ISYUL)k4UsVtO(05D3fsz{`X*PO-gUYH;BXyJj?hNW z>Nf}L`PPbpR)BLG0Dq^luhe{@1ZIg4QUF+@vYnFrk zxoO^;%6FWH{DYYAYwS`? zdFzahz8j$b*2%$C;f96lq1&}3&Kbj(JWERhxj^=LK#Tf4GLl~_8t%M4F6z?zA~DAu zlnj-`Oa;FGR5M&(7dU(ZE^b*HtNL75r{|(hZ3pBd2oRg@+Fn*|0A&n-(Ti)Yb;~(} zxBcy+a#hl1e--Qfo|tF{#0f+sd-S*Pf}|lGaH}D2g(uxJLHui$g3z_Hx(Y&lanJL0 z<#QcFLrjOAI3Ur&!a^MbgFj=nwH;26k3c}m#0+d4^sTw}0fh@ZuDKT4s-;X|3VJTj zU2xOZ(xUVPI|tL7h5{NE#QU*371(Vij9uH?j(}R3sFiYFRv+5(6B#Pdt^tpd_Z^M_Nnxfp!R@mNMNCc1(9&f2^4 z!Blts9#k6WI>Ro7wv!sOA`4TGnF~mX!0iIW=0TUi@hC9|uS_l6^p99ji!)uuu_#Q7 zAnpdi5YboV#*lzH#d~RLh&MC7SWpsyx}3TMdq2m5>5`v*QWl^OHXxRf5Y(vUS_4=q z>26oGPRyynNc5^>9Q3g+AB?;9zHnYeprQ5O%8SN7qYQ#LLI*`V@6&Fnr2rSBV*2kVR<%lLY}!is8BSe zLezU`0mCH?n(TKgxJJuNZ)J-_CZz%6UZL|rJ8vCN0WZ0lEmY3EV!#pb_pRl7Of7nED9N|s{8kR zGkvZf~Q4T=-puQDg6fCHu`NnkDSszRxlu0Ox;F`Q6 z;3(s|lB;dJj>GD5_9ZLIF1P1A0_lc`LHVb^lf5+GINQO5K>H#zAIZ}*-qIF(g5qIr z=InZ@t7S`baz+MiHwM_uOMS1Ryxg4=z}zKvg^ZquQ{EcEajd8^8=A(ALdU{Wq6eMz zp217Z$mqsvGMa~Yor_xT(KV4(cOV3QG(Z38iACF^^rz1r-*M>QCxd3n@mM6%)n&#T zgBtb=7dt6S$qJaVP8{JFQnN;r#|DPT?5qTtv*b;}$^#9297){iR!~slY5R_DKgzav z{AqEJsbw4jp}39j1#t3{C6nQ&rZF2|$+hV%JE}(qTG66{K^fuFUPhK}k%<4*$`r>x z3>-9)AyUN@K?>`!K#WH)T7<$8JN~gpu)BZf6@RZL6ntp4L{8V`ha_{IG3*aJ1KSDN zP!fU3DxZJAtYdN_u)BQ|oTV4~EQ~h$#(`7|6jXo{A-KjgJ;KZNIvRZ4x%I$|p*rb! zqR43ai{a8B8oRhuy<-Qfx&3G~&2%Kh>s2_x$HvymYOH1ly3Nl{YuI=33qA}xIF$V! zJaL2!GEqX9F(-17qOvsyoX|GkqpyJZJyMsa=O>$^bsTiF(xNRAEgibDoT&FA?MhZ! z%Q*q|l3)AoJ>Tswxg9OqY;-``vkfCj35>sX?b<7do#w=cTFZw^Bh|V$>P*FuLWouooGfFFPq@FXLF*{11 zmZ@mfOpy^{rvF(SPiZrzRmiEL9%ou}{UQhVJ4V=502x9oJK+^T6V^8CPeA>liT`%r z!{3h^Pz3*1XaYncr0oA6PE0CZym*oCZB>EotmQ*WQt-EdTO_00PeM zTUGCX=(Rpx)24tq7zuaH6CB;eaSH6@WB>+1&2pDcnet@_FG9mY6?b+lI7p zhTYb$wohMP_(2GizQxDDGBHns6G0AvJ|$8mv*QPp5%yu1t<{l={1+8vWp8<*D!C5@ zr(66l)@)FD3G!4|2Usp)_x=*0CMPE!Y_6@XY5r!)*ao>*v9#$F`}Bph{LWg{RNTLi zgW_f#vzq9I^WcoI zN^B?7*WnXGKS|_t>lX(SlZ`=x=Pe)$-Kf$7d3dN$+x#cf`L`g`-(C{UsQsG;Ws!*B zGGAX?V^kU#5ei`s5?0Q;%{2A*cS_BNyP{Jr_`NvaWFgLRXGOPsu~#+^IUZ zWSiiEuYg`?gfq~u?OfSnMF{~X{5b@O3?;@J-W+HkSrdf-@=O94;Wlg(-#nb}idQVg zW<~QUU5=htE)W2@CBTST46$#90c)lXe|-p)K1y#5y+5=e85Et?Eh;8o!BYe ztbpJ)+ZlUr`AoY+-zdQ;5W(+>4vtt3^@)gB02Fw5irW}E5EMHW3v!Oz`Gbfboy6b(+HYJ?l=I7=mrzI$*FtC724bcQC!irly1t zDVBQZKlxzly!u|kRQ{zbi2dN;9-{xZzNUY_zwvEQDXOYBTX^568~#b^1xY$J;NTRZ za*@y<2KJb}fg3VhOxlkDIAY9cZUz*8Vr9@B+9<${mqg-l+v3+7Hw2EoO%fM6nvBR% zd}8y)!5NntH?$|98MH)(7d%ogjv+|{jezn;*j4_4<$E(4=uzdIl!2-b3xo0JFLfs` z6ui`%ryGkvP{Mc^r@7IZGw^y%(d(*p$VHvKdHi%-WIRPUCm4OeE;>A*W9$}&zxn;Wu`cS*apBBZ61w+?!D>eHE> zdUrs+$DUPBksh7Iz{BtP4tN=F3TXi1iF`^KxGAm@pn!L}C=qCg$f`boAVx;tOgA&2 z28A+#;sq#DxYJ0*g7WK^(?DsBia=8WJqlr^hC}fw-N><6c%;A2x4K8h?|6GoJgkK( zkkOB!BQ}0-`s0^05yVVw2@Jy#N`zpXxnmQT7FI6@KKFp&koT+z*d=A0S9RkUFP(p9 z^l_|-;B^!W@azclUXaJ71y~y=vWfRf$C=vG%CD7HZ&(mM{IqbTf)kV+cR%j!aM4Nk zsE668J?t_A65??# zV@l+t#Zsm$xTiV!%ZqH4K4jp=q_Z#=&vGz9rL!y&>sgk-&Ekg%_!W{t_?&y{@sfi%%t8|rpK zJ2$$*Jgd{C*eaEd z2HF9QlAW<-7hZ5ay-Q@WB9yhgIby_asH;qqm?2$DxOC&*r#W5VwETX>_&HBCN zQk=FtJDDP)U%X??{>g7TtNS6X*gLgdR*nBrEZ%Lb__1-RtyENG)`~L0|Ek}EShp-e zElwjKy1ltr3`DR7Tu=Ga1}E-yX_zvcbt0?B>!WHFEp6pV+hYX)Rgqz@vWT%AD;zV= z%%s{nL)&eDIr+SE2(yc56;!(Q1BP-4jC+e(!&L7E2(+WZOdcZMlVlgO?}$yWrRX5}MnfMPlm#TqiDD3!{*)*ntGjZz;ni$1s7p+eqi`%@M zYIg@IBHmm1P+{+Osg_38JXqo|of9kO$3Z=CK;Bb=shZ& zV@^7s{K))JUQ749apO9%7s_D}+5T2C1gtW!2Z&h;M|L#>)a0VxM)%0U%z!%jBnfQj zax#07Tqfr8y1u&gDG#;oE@yw`PeH_#W!=29F}=4RZsns(mXo<~!&J2XRMpI}XW;#D z=c>HikYf+1lVa9)ody+qwiD4G^cdzmJ7^Lt9zA*_+v65v^hdH%u+T|siUE zy^HogW}AQCIAi{xX{xmLvXg&{Fz7uYf>_(yYK@aIW%MDD$bahv*>>v%%_fJ4&|u4a zP{rHW5JL{Ds1;vsD*aq?1`R0qHk38j&owpQAuQOw{r&quuqw2bGX_p6cs*bP;r*Z? z%k+UIjftYsH-GwqHnoO`^D9zPZjU#@X+Ud=rf@)>R$N>h1k&)LbT;l;;ZSRem>W>% zRwd6!Y%T-0Yh`8SZ$gHGOe_$!XkU0AneLr{oPEz$RiN^p`fZv?=}B)Z(4r1kD|#Ui zdauoMm^dIweU;yvtL09WcyoWqikzIBByeU)FqlZ(eUsrp0+ir{6jjggb|##yG(B@< zY>ehoBM`2x=*ei%0VZ798#YY)ZngNj5qQ>W5u`(;_$L93>gecjG@Jl4Ro+YBCdwfn z6}$|rgi0Z$4}803=5}`10a((x9|H>%u%teoC6y|4{I1?JGuKxD)RuS=&!Rx4dgb8Y zFzbr99zFM#q0eO?_8XH8%o6e%6ySnNeqca~nzJhne4hW&&>#US$T}j@+vmU>il1JN zHtiX?Q}H$+Og|5=LRsZlAh_JPxw*lSKO&Sjv-{=%Q9-O4ix;TSa?W_oDT_b%Wli)S zfeSTV7*yxBk=lursrUu}6c4xnEHBbEc^x_CYo$1TT4kHG5fb zo!Do8m8vbm5?{?5{l?gDSBRQpID~U{%v=lZ;aL=~e$_b2o}2;31O)t3Rn87{YZz>8 zZ1s(U+GWb;tO1*97UYBZ&_i}gxF3}&2@n07Iyvwd*xc#|tJxNzK=IKYW5}Qe)dm=t zqI{8pTR*Q>bZ1)9zx3VTgDHFKAF)I-r1)1f0pYXI_6e|h**vhjcWftW112U6fKX}# zcu&}N9JEG-S2i>dL)_8f!g#4`4|^gB_5moZ0)ZY2WYoPq7sT;5<4vV<;CuqY#E*4x zxdmobTB{%#kYXI z0`hWB9v@vVAuwm@n-DTK04CAk>>wOll=c}d*fU1&qEFyueb3AdV8eiBEvE+ z0m&*fqaP#)D9|0c%E!I`@RmI-My7XO1Sm}+=8>jU(6+Z5I224SiUl<(ultZBFKtMn z(kF^V;Uy)JAA`scn&kE^{|Lu^L0KSJ-NSb)L5E!;>Qx$cZUI|SP{~C1zGiSLk|Xq` z?yr30_nrEL<7atM zI;+;50q{|uWPw>nX0CSeLO~NA;Ef1Tgh2TT*5hN&A`B)#>yDjmV7- zG{`u`AQeP;707-giHBW=gkt*b09m4;Vp^ZM9E*T%#xVh?)sj07mdQcw-f&7J-_+;7ngHKytgahqcsmBTZBSLrN|kDEGe)xQG}n6z2#41Em>cdbE-cSDEZ1h zwq!(#+WM(#OAFK|z3?m9YmO3Ntikn>wEACoH6y#nphvM)VV^anausy|KWr?W)G*P_ zDT(l2-`@GK*2+r@^Nb}!#w5?{hk<@mZ0DY!lKM$9!g&0srbNt{TU3DzEKgUthW!(o zY>D>$E-R5xcA%Ai1}Y^9L4iN#m9}Rt?;gQ2Efzk{QIdiFI3NH4b#LX^M361>I-?OI zGua6rRr_}iBg#8YPsXyLV%8>T5d_Q-?@pI2KVuc&<+r^lYaGF~_N!L{D!nK7uw$p3 z$D+cTnK?@VMaCZ*tr}-UX_?j@nazofbbU-orYsb2;;Qs$zwgmzT1#Xhn+P=urbN>v7s_ zPRq#Qjy%rL3|}W6q`I6PKUs_wTe?n8z1FzgB*8nQJeVuoh0Wf5BJ0bWn`&%>-(Btn zCVKZoKbbO(5taH28qdk8#w5|itrSNK%eHTt1IrVfWy%hk=353Hkui}b4==sVkN@1Y z5}K`%P$g=kb|uzB_^6+rNBT*%T1Kp>B3wf=&Uq6!ehUx6#%N=Yy@Cv;${8KT(JC;h zlkSW5l+J1kG4PHxA*^;{4tp(0SPvcIa8bu(%U|-*N7osD=f-w~Mp~7qQoqNO%@Ck^ zqV#CziJ41dh0g;yvPQN>rF_+fQSC~MFnROzo@h_f`O6_j!*1m1std<&+f>swa6sC9 zxy$u$b|#Fx0ZrD9a8kR-HQzGZ@uYS4OP2<=KqKV`@io1ds>f$}QJIacny`I-3^VFy z6&Ia0`S16MR!PpZfP7!v%RJ_P=tFDTbN%h}Ii@g+T`TdTlX8{G!pCS{{IvUm-=!2e z(#jc%>dbk(u-!|4A2TP}I>2;KSpK$j9BO5TLN?Mem-93-GMKY!m_oZ_@28l}VOgWWz;j6rVX7PV+!x0A8z_FP@^HQn?|wLh zV=2XJ=8gN0mWK7tv=C@SZ@4d z8G2k@UF~uEHlU4JYvLwTM*81$Lm&xBz_B;~*3uRfe5rXRIrZe52S1z>1>dBDD99?ylstOr_rCx{njBOB literal 0 HcmV?d00001 diff --git a/images/layered_architecture_diagram.svg b/images/layered_architecture_diagram.svg deleted file mode 100644 index 5ba206ec..00000000 --- a/images/layered_architecture_diagram.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - Layered Architecture - - - - Application Layer - - - Scene Management Layer - - - Rendering Layer - - - Resource Management Layer - - - Platform Abstraction Layer - - - - - - - - - - - - - diff --git a/images/rendering_pipeline_flowchart.svg b/images/rendering_pipeline_flowchart.svg new file mode 100644 index 00000000..fa18ff39 --- /dev/null +++ b/images/rendering_pipeline_flowchart.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + Vulkan Rendering Pipeline + + + + Scene Data + + + + Scene Culling + Determine visible objects + + + + Visible Objects + + + + Render Pass Management + Organize rendering passes + + + + Rendergraph + Manage dependencies + + + + Synchronization + Barriers, semaphores + + + + Command Generation + Create GPU commands + + + + Command Buffers + + + + Execution + Submit to GPU queue + + + + Post-Processing + Apply visual effects + + + + Deferred + + + Forward+ + + + PBR + + + + Final Image + + + + + + + + + + + + + + + + + + + + + + + + + Legend + + + + Pipeline Stage + + + + Component + + + + Technique + + + + Resource + From 5ec2035dd3a615c6644a1a20735234165fc4075c Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 16:22:10 -0700 Subject: [PATCH 014/102] add the requested appendix.adoc --- .../Appendix/appendix.adoc | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 en/Building_a_Simple_Engine/Appendix/appendix.adoc diff --git a/en/Building_a_Simple_Engine/Appendix/appendix.adoc b/en/Building_a_Simple_Engine/Appendix/appendix.adoc new file mode 100644 index 00000000..db8bac0f --- /dev/null +++ b/en/Building_a_Simple_Engine/Appendix/appendix.adoc @@ -0,0 +1,304 @@ +:pp: {plus}{plus} + += Appendix: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Detailed Architectural Patterns + +This appendix provides in-depth information about common architectural patterns used in modern rendering and game engines. These patterns are referenced in the main Engine Architecture section, with a focus on Component-Based Architecture in the main tutorial. + +[[layered-architecture]] +=== Layered Architecture + +One of the most fundamental architectural patterns is the layered architecture, where the system is divided into distinct layers, each with a specific responsibility. + +image::../../../images/layered_architecture_diagram.svg[Layered Architecture Diagram, width=600] + +==== Typical Layers in a Rendering Engine + +1. *Platform Abstraction Layer* - Provides a consistent interface to platform-specific functionality. +2. *Resource Management Layer* - Manages loading, caching, and unloading of assets. +3. *Rendering Layer* - Handles the rendering pipeline, shaders, and graphics API interaction. +4. *Scene Management Layer* - Manages the scene graph, spatial partitioning, and culling. +5. *Application Layer* - Handles user input, game logic, and high-level application flow. + +==== Benefits of Layered Architecture + +* Clear separation of concerns +* Easier to understand and maintain +* Can replace or modify individual layers without affecting others +* Facilitates testing of individual layers + +==== Implementation Example + +[source,cpp] +---- +// Platform Abstraction Layer +class Platform { +public: + virtual void Initialize() = 0; + virtual void* CreateWindow(int width, int height) = 0; + virtual void ProcessEvents() = 0; + // ... +}; + +// Resource Management Layer +class ResourceManager { +public: + virtual Texture* LoadTexture(const std::string& path) = 0; + virtual Mesh* LoadMesh(const std::string& path) = 0; + // ... +}; + +// Rendering Layer +class Renderer { +public: + virtual void Initialize(Platform* platform) = 0; + virtual void RenderScene(Scene* scene) = 0; + // ... +}; + +// Scene Management Layer +class SceneManager { +public: + virtual void AddEntity(Entity* entity) = 0; + virtual void UpdateScene(float deltaTime) = 0; + // ... +}; + +// Application Layer +class Application { +private: + Platform* platform; + ResourceManager* resourceManager; + Renderer* renderer; + SceneManager* sceneManager; + +public: + void Run() { + platform->Initialize(); + renderer->Initialize(platform); + + // Main loop + while (running) { + platform->ProcessEvents(); + sceneManager->UpdateScene(deltaTime); + renderer->RenderScene(sceneManager->GetActiveScene()); + } + } +}; +---- + +[[data-oriented-design]] +=== Data-Oriented Design + +Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. + +image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=600] + +==== Key Concepts + +1. *Data Layout* - Organizing data for cache-friendly access patterns. +2. *Systems* - Process data in bulk, often using SIMD instructions. +3. *Entity-Component-System (ECS)* - A common implementation of DOD principles. + +==== Benefits of Data-Oriented Design + +* Better cache utilization +* More efficient memory usage +* Easier to parallelize +* Can lead to significant performance improvements + +==== Implementation Example + +[source,cpp] +---- +// A simple ECS implementation +struct TransformData { + std::vector positions; + std::vector rotations; + std::vector scales; +}; + +struct RenderData { + std::vector meshes; + std::vector materials; +}; + +class TransformSystem { +private: + TransformData& transformData; + +public: + TransformSystem(TransformData& data) : transformData(data) {} + + void Update(float deltaTime) { + // Process all transforms in bulk + for (size_t i = 0; i < transformData.positions.size(); ++i) { + // Update transforms + } + } +}; + +class RenderSystem { +private: + RenderData& renderData; + TransformData& transformData; + +public: + RenderSystem(RenderData& rData, TransformData& tData) + : renderData(rData), transformData(tData) {} + + void Render() { + // Render all entities in bulk + for (size_t i = 0; i < renderData.meshes.size(); ++i) { + // Render mesh with transform + } + } +}; +---- + +[[service-locator-pattern]] +=== Service Locator Pattern + +The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. + +image::../../../images/service_locator_pattern_diagram.svg[Service Locator Pattern Diagram, width=600] + +==== Key Concepts + +1. *Service Interface* - Defines the contract for a service. +2. *Service Provider* - Implements the service interface. +3. *Service Locator* - Provides access to services. + +==== Benefits of Service Locator Pattern + +* Decouples service consumers from service providers +* Allows for easy service replacement +* Facilitates testing with mock services + +==== Implementation Example + +[source,cpp] +---- +// Audio service interface +class IAudioService { +public: + virtual ~IAudioService() = default; + virtual void PlaySound(const std::string& soundName) = 0; + virtual void StopSound(const std::string& soundName) = 0; +}; + +// Concrete audio service +class OpenALAudioService : public IAudioService { +public: + void PlaySound(const std::string& soundName) override { + // Implementation using OpenAL + } + + void StopSound(const std::string& soundName) override { + // Implementation using OpenAL + } +}; + +// Service locator +class ServiceLocator { +private: + static IAudioService* audioService; + static IAudioService nullAudioService; // Default null service + +public: + static void Initialize() { + audioService = &nullAudioService; + } + + static IAudioService& GetAudioService() { + return *audioService; + } + + static void ProvideAudioService(IAudioService* service) { + if (service == nullptr) { + audioService = &nullAudioService; + } else { + audioService = service; + } + } +}; + +// Usage example +void PlayGameSound() { + ServiceLocator::GetAudioService().PlaySound("explosion"); +} +---- + +=== Comparative Analysis of Architectural Patterns + +Below is a comparative analysis of the architectural patterns discussed in this appendix: + +|=== +| Pattern | Strengths | Weaknesses | Best Used For + +| Layered Architecture +| * Clear separation of concerns + * Easy to understand + * Good for beginners +| * Can lead to "layer bloat" + * May introduce unnecessary indirection + * Potential performance overhead from layer traversal +| * Smaller engines + * Educational projects + * When clarity is more important than performance + +| Component-Based Architecture +| * Highly flexible and modular + * Promotes code reuse + * Avoids deep inheritance hierarchies + * Easier to extend with new features +| * More complex to implement initially + * Can be harder to debug + * Potential performance overhead from component lookups +| * Modern rendering engines + * Systems with diverse object types + * Projects requiring frequent extension + +| Data-Oriented Design +| * Excellent performance + * Cache-friendly memory access + * Good for parallel processing +| * Less intuitive than OOP + * Steeper learning curve + * Can make code harder to read +| * Performance-critical systems + * Mobile platforms + * Systems processing large amounts of similar data + +| Service Locator Pattern +| * Decouples service providers from consumers + * Facilitates testing + * Allows runtime service swapping +| * Can hide dependencies + * Potential for runtime errors + * Global state concerns +| * Cross-cutting concerns + * Systems requiring runtime configuration + * When loose coupling is critical +|=== + +== Conclusion + +These architectural patterns provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. + +When designing your engine architecture, consider: + +1. *Performance Requirements* - Different patterns have different performance characteristics. +2. *Flexibility Needs* - How much flexibility do you need for future extensions? +3. *Team Size and Experience* - More complex architectures may be harder to work with for smaller teams. +4. *Project Scope* - A small project may not need the complexity of a full ECS. + +link:../Engine_Architecture/02_architectural_patterns.adoc[Back to Architectural Patterns] From 31ded49af93c42926c06675f8adeb2da18869113 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 18:29:15 -0700 Subject: [PATCH 015/102] add the requested appendix.adoc --- .../Engine_Architecture/02_architectural_patterns.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index 88ee9a46..18daca86 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -23,8 +23,7 @@ Here's a brief introduction to the most common architectural patterns used in ga Layered architecture divides the system into distinct layers, each with a specific responsibility. Typical layers include platform abstraction, resource management, rendering, scene management, and application layers. -image::../../../images/layered_architecture_diagram.png[Layered Architecture -Diagram, width=400] +image::../../../images/layered_architecture_diagram.png[Layered Architecture Diagram, width=400] *Key Benefits:* * Clear separation of concerns From 659ecb12af3a79966fd20d1d1177bde1921b9fff Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 20:46:00 -0700 Subject: [PATCH 016/102] address the requested items --- .../Appendix/appendix.adoc | 33 +++++++++++++++++- .../02_architectural_patterns.adoc | 16 +++++++-- .../05_rendering_pipeline.adoc | 34 +++++++------------ .../Engine_Architecture/06_event_systems.adoc | 10 ++++-- .../Engine_Architecture/conclusion.adoc | 12 +++---- 5 files changed, 72 insertions(+), 33 deletions(-) diff --git a/en/Building_a_Simple_Engine/Appendix/appendix.adoc b/en/Building_a_Simple_Engine/Appendix/appendix.adoc index db8bac0f..11825732 100644 --- a/en/Building_a_Simple_Engine/Appendix/appendix.adoc +++ b/en/Building_a_Simple_Engine/Appendix/appendix.adoc @@ -290,9 +290,39 @@ Below is a comparative analysis of the architectural patterns discussed in this * When loose coupling is critical |=== +== Advanced Rendering Techniques + +This section provides an overview of advanced rendering techniques commonly used in modern rendering engines. For more comprehensive information, refer to these excellent resources: + +* *Physically Based Rendering: From Theory to Implementation* - https://www.pbr-book.org/ +* *Real-Time Rendering* - https://www.realtimerendering.com/ +* *GPU Gems* series - https://developer.nvidia.com/gpugems/gpugems/contributors + +=== Deferred Rendering + +Deferred rendering separates the geometry and lighting calculations into separate passes, which can be more efficient for scenes with many lights: + +1. *Geometry Pass* - Render scene geometry to G-buffer textures (position, normal, albedo, etc.). +2. *Lighting Pass* - Apply lighting calculations using G-buffer textures. + +=== Forward+ Rendering + +Forward+ (or tiled forward) rendering combines the simplicity of forward rendering with some of the efficiency benefits of deferred rendering: + +1. *Light Culling Pass* - Divide the screen into tiles and determine which lights affect each tile. +2. *Forward Rendering Pass* - Render scene geometry with only the lights that affect each tile. + +=== Physically Based Rendering (PBR) + +PBR aims to create more realistic materials by simulating how light interacts with surfaces in the real world: + +1. *Material Parameters* - Define materials using physically meaningful parameters (albedo, metalness, roughness, etc.). +2. *BRDF* - Use a physically based bidirectional reflectance distribution function. +3. *Image-Based Lighting* - Use environment maps for ambient lighting. + == Conclusion -These architectural patterns provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. +These architectural patterns and rendering techniques provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. When designing your engine architecture, consider: @@ -302,3 +332,4 @@ When designing your engine architecture, consider: 4. *Project Scope* - A small project may not need the complexity of a full ECS. link:../Engine_Architecture/02_architectural_patterns.adoc[Back to Architectural Patterns] +link:../Engine_Architecture/05_rendering_pipeline.adoc[Back to Rendering Pipeline] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index 18daca86..309ba11e 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -23,7 +23,7 @@ Here's a brief introduction to the most common architectural patterns used in ga Layered architecture divides the system into distinct layers, each with a specific responsibility. Typical layers include platform abstraction, resource management, rendering, scene management, and application layers. -image::../../../images/layered_architecture_diagram.png[Layered Architecture Diagram, width=400] +image::../../../images/layered_architecture_diagram.png[Layered Architecture Diagram, width=400, alt="Layered Architecture Diagram showing different layers of a rendering engine"] *Key Benefits:* * Clear separation of concerns @@ -62,7 +62,19 @@ For detailed information and implementation examples, see the link:../Appendix/a Component-based architecture is widely used in modern game engines and forms the foundation of our Vulkan rendering engine. It promotes composition over inheritance and allows for more flexible entity design. -image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600] +image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600, alt="Component-Based Architecture Diagram showing entities, components, and systems"] + +[NOTE] +==== +*Diagram Legend:* + +* *Boxes*: Blue boxes represent Entities, orange boxes represent Components, and green boxes represent Systems +* *Line Types*: + ** Dashed lines show ownership/containment (Entities contain Components) + ** Solid lines show processing relationships (Systems process specific Components) +* *Text*: All text elements use dark colors for visibility in both light and dark modes +* *Directional Flow*: Arrows indicate the direction of relationships between elements +==== ==== Key Concepts diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 9a799490..aebc8f3f 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -17,7 +17,17 @@ A well-designed rendering pipeline is essential for creating a flexible and effi The following diagram provides a high-level overview of a modern Vulkan rendering pipeline: -image::../../../images/rendering_pipeline_flowchart.svg[Rendering Pipeline Flowchart, width=600] +image::../../../images/rendering_pipeline_flowchart.svg[Rendering Pipeline Flowchart, width=600, alt="Flowchart showing the stages of a modern Vulkan rendering pipeline"] + +[NOTE] +==== +*Diagram Legend:* + +* *Boxes*: Represent the different stages of the rendering pipeline +* *Arrows*: Show the flow of data and execution between stages +* *Colors*: Different colors indicate different types of operations (processing, management, execution) +* *Supporting Components*: Rendergraphs and Synchronization primitives are shown as connected to the main pipeline flow +==== The rendering pipeline consists of several key stages: @@ -1221,27 +1231,7 @@ private: === Advanced Rendering Techniques -==== Deferred Rendering - -Deferred rendering separates the geometry and lighting calculations into separate passes, which can be more efficient for scenes with many lights: - -1. *Geometry Pass* - Render scene geometry to G-buffer textures (position, normal, albedo, etc.). -2. *Lighting Pass* - Apply lighting calculations using G-buffer textures. - -==== Forward+ Rendering - -Forward+ (or tiled forward) rendering combines the simplicity of forward rendering with some of the efficiency benefits of deferred rendering: - -1. *Light Culling Pass* - Divide the screen into tiles and determine which lights affect each tile. -2. *Forward Rendering Pass* - Render scene geometry with only the lights that affect each tile. - -==== Physically Based Rendering (PBR) - -PBR aims to create more realistic materials by simulating how light interacts with surfaces in the real world: - -1. *Material Parameters* - Define materials using physically meaningful parameters (albedo, metalness, roughness, etc.). -2. *BRDF* - Use a physically based bidirectional reflectance distribution function. -3. *Image-Based Lighting* - Use environment maps for ambient lighting. +For detailed information about advanced rendering techniques such as Deferred Rendering, Forward+ Rendering, and Physically Based Rendering (PBR), please refer to the link:../Appendix/appendix.adoc#advanced-rendering-techniques[Advanced Rendering Techniques] section in the Appendix. This section includes references to valuable resources for further reading. === Conclusion diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc index 7199ebeb..05e742ff 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc @@ -15,7 +15,7 @@ Event systems provide a flexible way for different parts of your engine to commu === The Need for Event Systems -In a complex engine, many subsystems need to communicate with each other: +Even in the simple engine we're building, subsystems need to communicate with each other efficiently. As our engine grows, these communication needs become increasingly important: 1. *Physics* needs to notify *Audio* when collisions occur. 2. *Input* needs to notify *Game Logic* when buttons are pressed. @@ -464,7 +464,13 @@ public: ==== Event Bubbling and Capturing -For hierarchical systems like UI, events can bubble up or capture down the hierarchy: +In hierarchical systems like UI, events can propagate through the hierarchy in two ways: + +* *Event Bubbling* - The event starts at the target element and "bubbles up" through parent elements in the hierarchy. For example, a click event on a button first triggers on the button, then on its container, and continues up to the root element. + +* *Event Capturing* - The event starts at the root element and travels down the hierarchy to the target element (the opposite direction of bubbling). + +This approach allows parent elements to intercept and handle events triggered on their children, while also giving children the ability to stop propagation if needed. For hierarchical systems like UI, this provides a flexible way to handle events at the appropriate level: [source,cpp] ---- diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc index 589bd76d..ac33e5ce 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc @@ -43,17 +43,17 @@ As you develop your own rendering engine, keep these principles in mind: === Next Steps -With a solid understanding of engine architecture, you're now ready to implement these concepts in your own rendering engine. Here are some suggestions for next steps: +Throughout this tutorial, we've been building a simple engine together, exploring key concepts and implementing core components. As we continue through the tutorial, we'll build upon these architectural foundations. Here are some suggestions for enhancing your learning: -1. *Implement a Prototype* - Start by implementing a simple prototype that incorporates the key architectural patterns we've discussed. +1. *Follow Along with the Code* - As you progress through the tutorial, implement the code examples to build your engine step by step. Each section contains code snippets that you can use as a reference. -2. *Experiment with Different Approaches* - Try different variations of these patterns to see what works best for your specific needs. +2. *Experiment with Variations* - Once you understand a concept, try modifying the implementation to see how different approaches affect your engine. -3. *Explore Advanced Topics* - Dive deeper into specific areas like advanced rendering techniques, physics integration, or audio systems. +3. *Explore Advanced Topics* - The link:../Appendix/appendix.adoc[Appendix] contains additional information on advanced rendering techniques and architectural patterns. -4. *Study Existing Engines* - Examine open-source engines to see how they solve similar problems. +4. *Study Existing Engines* - Examine open-source engines like link:https://github.com/TheCherno/Hazel[Hazel] or link:https://github.com/LWJGL/lwjgl3[LWJGL] to see how they implement similar concepts. -5. *Join the Community* - Engage with the graphics programming community to share ideas and get feedback on your approach. +5. *Join the Community* - Engage with the graphics programming community to share ideas and get feedback on your implementation. Remember that engine development is an iterative process. Your architecture will evolve as you gain experience and as your requirements change. The concepts we've covered provide a foundation, but the best architecture for your engine will depend on your specific goals and constraints. From bd98b2da52bb92973a2f29bdb0d20e1cd5e12f2f Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 21:00:10 -0700 Subject: [PATCH 017/102] address the requested items --- .../Appendix/appendix.adoc | 11 + .../02_math_foundations.adoc | 62 ++- .../03_camera_implementation.adoc | 96 +++- .../03_transformation_matrices.adoc | 182 +++++++ .../04_camera_implementation.adoc | 490 ++++++++++++++++++ .../05_vulkan_integration.adoc | 10 + 6 files changed, 818 insertions(+), 33 deletions(-) create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc create mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc diff --git a/en/Building_a_Simple_Engine/Appendix/appendix.adoc b/en/Building_a_Simple_Engine/Appendix/appendix.adoc index 11825732..38c6ce1c 100644 --- a/en/Building_a_Simple_Engine/Appendix/appendix.adoc +++ b/en/Building_a_Simple_Engine/Appendix/appendix.adoc @@ -320,6 +320,17 @@ PBR aims to create more realistic materials by simulating how light interacts wi 2. *BRDF* - Use a physically based bidirectional reflectance distribution function. 3. *Image-Based Lighting* - Use environment maps for ambient lighting. +=== Advanced Camera Techniques + +This section covers advanced techniques for implementing sophisticated camera systems in 3D applications: + +* *Camera Collision*: Implement a collision volume for the camera to prevent it from passing through walls +* *Context-Aware Positioning*: Adjust camera position based on the environment (e.g., zoom out in large open areas, zoom in in tight spaces) +* *Intelligent Framing*: Adjust the camera to keep both the character and important objects in frame +* *Predictive Following*: Anticipate character movement to reduce camera lag +* *Camera Obstruction Transparency*: Make objects that obstruct the view partially transparent +* *Dynamic Field of View*: Adjust the FOV based on movement speed or environmental context + == Conclusion These architectural patterns and rendering techniques provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc index 0c678a2d..9ae66c24 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc @@ -38,9 +38,40 @@ In our camera system, vectors serve several critical purposes: * *Dot Product*: Calculates the cosine of the angle between vectors (when normalized) - Applications: Determining if objects are facing the camera, calculating lighting intensity +==== The Right-Hand Rule + +The right-hand rule is a convention used in 3D graphics and mathematics to determine the orientation of coordinate systems and the direction of cross products. + +* *For Cross Products*: When calculating A × B: + + 1. Point your right hand's index finger in the direction of vector A + 2. Point your middle finger in the direction of vector B (perpendicular to A) + 3. Your thumb now points in the direction of the resulting cross product + +* *For Coordinate Systems*: In a right-handed coordinate system: + + 1. Point your right hand's index finger along the positive X-axis + 2. Point your middle finger along the positive Y-axis + 3. Your thumb points along the positive Z-axis + +[source,cpp] +---- +// The cross product direction follows the right-hand rule +glm::vec3 xAxis(1.0f, 0.0f, 0.0f); // Point right (positive X) +glm::vec3 yAxis(0.0f, 1.0f, 0.0f); // Point up (positive Y) + +// Cross product gives the Z axis in a right-handed system +glm::vec3 zAxis = glm::cross(xAxis, yAxis); // Points forward (positive Z) +// zAxis will be (0.0f, 0.0f, 1.0f) + +// If we reverse the order, we get the opposite direction +glm::vec3 negativeZ = glm::cross(yAxis, xAxis); // Points backward (negative Z) +// negativeZ will be (0.0f, 0.0f, -1.0f) +---- + * *Cross Product*: Creates a vector perpendicular to two input vectors - Applications: Generating the camera's "right" vector from "forward" and "up" vectors - - The direction follows the right-hand rule (explained below) + - The direction follows the right-hand rule (explained above) * *Normalization*: Preserves direction while setting length to 1 - Applications: Ensuring consistent movement speed regardless of direction @@ -192,6 +223,7 @@ Vulkan works with both row-major and column-major formats, but you need to speci * GLM (commonly used with Vulkan) uses column-major by default, but can be configured for row-major The practical implications: + * Matrix multiplication order may need to be reversed depending on the layout * When debugging, matrix elements may appear transposed compared to mathematical notation * When porting code between different APIs, matrix layouts may need to be transposed @@ -203,11 +235,18 @@ Affine transformations are a fundamental concept in computer graphics that prese ==== Properties of Affine Transformations An affine transformation can be represented as a combination of: + * Linear transformations (rotation, scaling, shearing) * Translation (movement) In mathematical terms, an affine transformation can be expressed as: -f(x) = Ax + b, where A is a matrix (linear transformation) and b is a vector (translation). + +[stem] +++++ +f(x) = Ax + b +++++ + +where A is a matrix (linear transformation) and b is a vector (translation). ==== Why Affine Transformations Matter in Graphics @@ -266,7 +305,7 @@ A pose matrix combines rotation and translation in a single 4×4 matrix: // Creating a pose matrix in GLM glm::mat4 poseMatrix = glm::mat4(1.0f); // Start with identity matrix poseMatrix = glm::translate(poseMatrix, position); // Apply translation -poseMatrix = poseMatrix * glm::mat4_cast(orientation); // Apply rotation (from quaternion) +poseMatrix = poseMatrix * rotationMatrix; // Apply rotation ---- ==== Applications of Pose Matrices @@ -813,7 +852,12 @@ void calculateFrustumCorners( ==== Projection and Unprojection -Projection converts 3D world coordinates to 2D screen coordinates, while unprojection does the reverse: +Projection converts 3D world coordinates to 2D screen coordinates, while unprojection does the reverse. The following code examples demonstrate these concepts for educational purposes: + +[NOTE] +==== +These utility functions are provided to help understand the mathematical concepts behind projection and unprojection. While they may not be directly used in the basic rendering pipeline, they are valuable for implementing features like object picking, mouse interaction with 3D objects, and custom rendering techniques. +==== [source,cpp] ---- @@ -1046,14 +1090,6 @@ If you're finding some of the mathematical concepts challenging or want to deepe - https://www.khronos.org/opengl/wiki/Coordinate_Transformations[OpenGL Wiki: Coordinate Transformations] - Reference for coordinate transformations - https://docs.microsoft.com/en-us/windows/win32/direct3d9/coordinate-systems[Microsoft Docs: Coordinate Systems] - Explanation of left-handed vs. right-handed systems -==== Vulkan-Specific Resources - -* *Official Documentation*: - - https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html[Vulkan Specification] - Official reference (see sections on coordinate systems) - - https://github.com/KhronosGroup/Vulkan-Guide[Khronos Vulkan Guide] - Official guide with explanations of Vulkan concepts - -* *Tutorials*: - - https://vkguide.dev/[Vulkan Guide] - Modern Vulkan tutorial with explanations of math concepts ==== GLM Library (Used in our examples) @@ -1077,4 +1113,4 @@ If you're finding some of the mathematical concepts challenging or want to deepe These resources should help you gain a deeper understanding of the mathematical concepts used in 3D graphics and camera systems. If you're struggling with a particular concept, try looking at multiple resources as different explanations might resonate better with your learning style. -link:03_camera_implementation.adoc[Next: Camera Implementation] +link:03_transformation_matrices.adoc[Next: Transformation Matrices] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc index e80a41b2..9763acbd 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc @@ -11,7 +11,7 @@ == Camera Implementation -Now that we understand the mathematical foundations, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. +Now that we understand the mathematical foundations, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. This implementation is designed for a general-purpose 3D application or game engine, and the concepts can be applied to various types of applications, from first-person games to architectural visualization tools. === Camera Types @@ -120,6 +120,68 @@ void Camera::processKeyboard(CameraMovement direction, float deltaTime) { } ---- +==== Handling Input Events + +The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW (a common windowing library used with Vulkan): + +[source,cpp] +---- +// In your application's input handling function +void processInput(GLFWwindow* window, Camera& camera, float deltaTime) { + // Keyboard input for camera movement + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); +} + +// Mouse callback function for camera rotation +void mouseCallback(GLFWwindow* window, double xpos, double ypos) { + static bool firstMouse = true; + static float lastX = 0.0f, lastY = 0.0f; + + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; // Reversed: y ranges bottom to top + + lastX = xpos; + lastY = ypos; + + // Pass the mouse movement to the camera + camera.processMouseMovement(xoffset, yoffset); +} + +// Scroll callback for zoom +void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + camera.processMouseScroll(yoffset); +} + +// Setting up the callbacks in your initialization code +void setupInputCallbacks(GLFWwindow* window) { + glfwSetCursorPosCallback(window, mouseCallback); + glfwSetScrollCallback(window, scrollCallback); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // Capture mouse +} +---- + +[NOTE] +==== +The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. +==== + === Camera Rotation For camera rotation, we'll use mouse input to adjust the yaw and pitch angles: @@ -330,6 +392,15 @@ This implementation: 3. Ensures the camera doesn't get too close to the character 4. Updates the camera orientation to maintain focus on the character +===== Performance Considerations for Occlusion Avoidance + +When implementing occlusion avoidance, be mindful of performance: + +* *Use simplified collision geometry*: For raycasting, use simpler collision shapes than your rendering geometry +* *Limit the frequency of occlusion checks*: You may not need to check every frame on slower devices +* *Consider spatial partitioning*: Use structures like octrees to accelerate raycasts by quickly eliminating objects that can't possibly intersect with the ray +* *Optimize for mobile platforms*: For performance-constrained devices, consider simplifying the occlusion algorithm or reducing its precision + ==== Implementing Orbit Controls Many third-person games allow the player to orbit the camera around the character. Here's how to implement this functionality: @@ -409,25 +480,10 @@ void gameLoop(float deltaTime) { } ---- -==== Advanced Techniques - -For even more sophisticated third-person cameras, consider these advanced techniques: - -* *Camera Collision*: Implement a collision volume for the camera to prevent it from passing through walls -* *Context-Aware Positioning*: Adjust camera position based on the environment (e.g., zoom out in large open areas, zoom in in tight spaces) -* *Intelligent Framing*: Adjust the camera to keep both the character and important objects in frame -* *Predictive Following*: Anticipate character movement to reduce camera lag -* *Camera Obstruction Transparency*: Make objects that obstruct the view partially transparent -* *Dynamic Field of View*: Adjust the FOV based on movement speed or environmental context - -==== Performance Considerations - -When implementing occlusion avoidance, be mindful of performance: - -* Use simplified collision geometry for raycasting -* Limit the frequency of occlusion checks -* Consider using spatial partitioning structures (e.g., octrees) to accelerate raycasts -* For mobile or performance-constrained platforms, simplify the occlusion algorithm +[NOTE] +==== +For more advanced camera techniques, refer to the Advanced Camera Techniques section in the Appendix. +==== In the next section, we'll explore how to use transformation matrices to position objects in our 3D scene. diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc new file mode 100644 index 00000000..1d66ad2e --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc @@ -0,0 +1,182 @@ +::pp: {plus}{plus} + += Camera & Transformations: Transformation Matrices +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Transformation Matrices + +In this section, we'll dive deeper into the transformation matrices used in 3D graphics and how they're applied in our rendering pipeline. + +=== The Model-View-Projection (MVP) Pipeline + +The transformation of vertices from object space to screen space involves a series of matrix multiplications, commonly known as the MVP pipeline: + +[source,cpp] +---- +// The complete transformation pipeline +glm::mat4 MVP = projectionMatrix * viewMatrix * modelMatrix; +---- + +Let's explore each of these matrices in detail. + +=== Model Matrix + +The model matrix transforms vertices from object space to world space. It positions, rotates, and scales objects in the world. + +[source,cpp] +---- +glm::mat4 createModelMatrix( + const glm::vec3& position, + const glm::vec3& rotation, + const glm::vec3& scale +) { + // Start with identity matrix + glm::mat4 model = glm::mat4(1.0f); + + // Apply transformations in order: scale, rotate, translate + model = glm::translate(model, position); + + // Apply rotations around each axis + model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + + // Apply scaling + model = glm::scale(model, scale); + + return model; +} +---- + +=== View Matrix + +The view matrix transforms vertices from world space to view space (camera space). It represents the position and orientation of the camera. + +[source,cpp] +---- +glm::mat4 createViewMatrix( + const glm::vec3& cameraPosition, + const glm::vec3& cameraTarget, + const glm::vec3& upVector +) { + return glm::lookAt(cameraPosition, cameraTarget, upVector); +} +---- + +The `lookAt` function creates a view matrix that positions the camera at `cameraPosition`, looking at `cameraTarget`, with `upVector` defining the up direction. + +=== Projection Matrix + +The projection matrix transforms vertices from view space to clip space. It defines how 3D coordinates are projected onto the 2D screen. + +==== Perspective Projection + +Perspective projection simulates how objects appear smaller as they get farther away, which is how our eyes naturally perceive the world. + +[source,cpp] +---- +glm::mat4 createPerspectiveMatrix( + float fovY, + float aspectRatio, + float nearPlane, + float farPlane +) { + return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); +} +---- + +Parameters: +* `fovY`: Field of view angle in degrees (vertical) +* `aspectRatio`: Width divided by height of the viewport +* `nearPlane`: Distance to the near clipping plane +* `farPlane`: Distance to the far clipping plane + +==== Orthographic Projection + +Orthographic projection doesn't have perspective distortion, making it useful for 2D rendering or technical drawings. + +[source,cpp] +---- +glm::mat4 createOrthographicMatrix( + float left, + float right, + float bottom, + float top, + float nearPlane, + float farPlane +) { + return glm::ortho(left, right, bottom, top, nearPlane, farPlane); +} +---- + +=== Normal Matrix + +When applying non-uniform scaling to objects, normals can become incorrect if transformed with the model matrix. The normal matrix solves this issue: + +[source,cpp] +---- +glm::mat3 createNormalMatrix(const glm::mat4& modelMatrix) { + // The normal matrix is the transpose of the inverse of the upper-left 3x3 part of the model matrix + return glm::transpose(glm::inverse(glm::mat3(modelMatrix))); +} +---- + +=== Applying Transformations in Shaders + +In Vulkan, we typically pass these matrices to our shaders as uniform variables: + +[source,glsl] +---- +// Vertex shader +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragNormal; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + // Apply MVP transformation + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + + // Transform normal using normal matrix + mat3 normalMatrix = transpose(inverse(mat3(ubo.model))); + fragNormal = normalMatrix * inNormal; + + fragTexCoord = inTexCoord; +} +---- + +=== Hierarchical Transformations + +For complex objects or scenes with parent-child relationships, we use hierarchical transformations: + +[source,cpp] +---- +// Parent transformation +glm::mat4 parentModel = createModelMatrix(parentPosition, parentRotation, parentScale); + +// Child transformation relative to parent +glm::mat4 localModel = createModelMatrix(childLocalPosition, childLocalRotation, childLocalScale); + +// Combined transformation +glm::mat4 childWorldModel = parentModel * localModel; +---- + +In the next section, we'll implement a camera system that uses these transformation concepts to navigate our 3D scenes. + +link:04_camera_implementation.adoc[Next: Camera Implementation] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc new file mode 100644 index 00000000..fdae12da --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc @@ -0,0 +1,490 @@ +::pp: {plus}{plus} + += Camera & Transformations: Camera Implementation +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Camera Implementation + +Now that we understand the mathematical foundations and transformation matrices, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. This implementation is designed for a general-purpose 3D application or game engine, and the concepts can be applied to various types of applications, from first-person games to architectural visualization tools. + +=== Camera Types + +There are several types of cameras commonly used in 3D applications: + +* *First-Person Camera*: Simulates viewing the world through the eyes of a character. +* *Third-Person Camera*: Follows a character from behind or another fixed position. +* *Orbit Camera*: Rotates around a fixed point, useful for object inspection. +* *Free Camera*: Allows unrestricted movement in all directions. + +For our implementation, we'll focus on a versatile camera that can be configured for different use cases. + +=== Camera Class Design + +Let's design a camera class that encapsulates the necessary functionality: + +[source,cpp] +---- +class Camera { +private: + // Camera position and orientation + glm::vec3 position; + glm::vec3 front; + glm::vec3 up; + glm::vec3 right; + glm::vec3 worldUp; + + // Euler angles + float yaw; + float pitch; + + // Camera options + float movementSpeed; + float mouseSensitivity; + float zoom; + + // Update camera vectors based on Euler angles + void updateCameraVectors(); + +public: + // Constructor with default values + Camera( + glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), + float yaw = -90.0f, + float pitch = 0.0f + ); + + // Get view matrix + glm::mat4 getViewMatrix() const; + + // Get projection matrix + glm::mat4 getProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 100.0f) const; + + // Process keyboard input for camera movement + void processKeyboard(CameraMovement direction, float deltaTime); + + // Process mouse movement for camera rotation + void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); + + // Process mouse scroll for zoom + void processMouseScroll(float yOffset); + + // Getters for camera properties + glm::vec3 getPosition() const { return position; } + glm::vec3 getFront() const { return front; } + float getZoom() const { return zoom; } +}; +---- + +=== Camera Movement + +We'll define an enum for camera movement directions: + +[source,cpp] +---- +enum class CameraMovement { + FORWARD, + BACKWARD, + LEFT, + RIGHT, + UP, + DOWN +}; +---- + +And implement the movement logic: + +[source,cpp] +---- +void Camera::processKeyboard(CameraMovement direction, float deltaTime) { + float velocity = movementSpeed * deltaTime; + + if (direction == CameraMovement::FORWARD) + position += front * velocity; + if (direction == CameraMovement::BACKWARD) + position -= front * velocity; + if (direction == CameraMovement::LEFT) + position -= right * velocity; + if (direction == CameraMovement::RIGHT) + position += right * velocity; + if (direction == CameraMovement::UP) + position += up * velocity; + if (direction == CameraMovement::DOWN) + position -= up * velocity; +} +---- + +==== Handling Input Events + +The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW (a common windowing library used with Vulkan): + +[source,cpp] +---- +// In your application's input handling function +void processInput(GLFWwindow* window, Camera& camera, float deltaTime) { + // Keyboard input for camera movement + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); +} + +// Mouse callback function for camera rotation +void mouseCallback(GLFWwindow* window, double xpos, double ypos) { + static bool firstMouse = true; + static float lastX = 0.0f, lastY = 0.0f; + + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; // Reversed: y ranges bottom to top + + lastX = xpos; + lastY = ypos; + + // Pass the mouse movement to the camera + camera.processMouseMovement(xoffset, yoffset); +} + +// Scroll callback for zoom +void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + camera.processMouseScroll(yoffset); +} + +// Setting up the callbacks in your initialization code +void setupInputCallbacks(GLFWwindow* window) { + glfwSetCursorPosCallback(window, mouseCallback); + glfwSetScrollCallback(window, scrollCallback); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // Capture mouse +} +---- + +[NOTE] +==== +The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. +==== + +=== Camera Rotation + +For camera rotation, we'll use mouse input to adjust the yaw and pitch angles: + +[source,cpp] +---- +void Camera::processMouseMovement(float xOffset, float yOffset, bool constrainPitch) { + xOffset *= mouseSensitivity; + yOffset *= mouseSensitivity; + + yaw += xOffset; + pitch += yOffset; + + // Constrain pitch to avoid flipping + if (constrainPitch) { + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + } + + // Update camera vectors based on updated Euler angles + updateCameraVectors(); +} +---- + +=== Updating Camera Vectors + +After changing the camera's orientation, we need to recalculate the front, right, and up vectors: + +[source,cpp] +---- +void Camera::updateCameraVectors() { + // Calculate the new front vector + glm::vec3 newFront; + newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); + newFront.y = sin(glm::radians(pitch)); + newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); + front = glm::normalize(newFront); + + // Recalculate the right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +=== View Matrix + +The view matrix transforms world coordinates into view coordinates (camera space): + +[source,cpp] +---- +glm::mat4 Camera::getViewMatrix() const { + return glm::lookAt(position, position + front, up); +} +---- + +=== Projection Matrix + +The projection matrix transforms view coordinates into clip coordinates: + +[source,cpp] +---- +glm::mat4 Camera::getProjectionMatrix(float aspectRatio, float nearPlane, float farPlane) const { + return glm::perspective(glm::radians(zoom), aspectRatio, nearPlane, farPlane); +} +---- + +=== Advanced Topics: Third-Person Camera Implementation + +In this section, we'll explore advanced techniques for implementing a third-person camera that follows a character while avoiding occlusion and maintaining focus on the character. + +==== Third-Person Camera Design + +A third-person camera typically needs to: + +1. Follow the character at a specified distance +2. Maintain a consistent view of the character +3. Avoid being occluded by objects in the environment +4. Provide smooth transitions during movement and rotation + +Let's extend our camera class to support these features: + +[source,cpp] +---- +class ThirdPersonCamera : public Camera { +private: + // Target (character) properties + glm::vec3 targetPosition; + glm::vec3 targetForward; + + // Camera configuration + float followDistance; + float followHeight; + float followSmoothness; + + // Occlusion avoidance + float minDistance; + float raycastDistance; + + // Internal state + glm::vec3 desiredPosition; + glm::vec3 smoothDampVelocity; + +public: + ThirdPersonCamera( + float followDistance = 5.0f, + float followHeight = 2.0f, + float followSmoothness = 0.1f, + float minDistance = 1.0f + ); + + // Update camera position based on target + void updatePosition(const glm::vec3& targetPos, const glm::vec3& targetFwd, float deltaTime); + + // Handle occlusion avoidance + void handleOcclusion(const Scene& scene); + + // Orbit around target + void orbit(float horizontalAngle, float verticalAngle); + + // Setters for camera properties + void setFollowDistance(float distance) { followDistance = distance; } + void setFollowHeight(float height) { followHeight = height; } + void setFollowSmoothness(float smoothness) { followSmoothness = smoothness; } +}; +---- + +==== Character Following Algorithm + +The core of a third-person camera is the algorithm that positions the camera relative to the character. Here's an implementation of the `updatePosition` method: + +[source,cpp] +---- +void ThirdPersonCamera::updatePosition( + const glm::vec3& targetPos, + const glm::vec3& targetFwd, + float deltaTime +) { + // Update target properties + targetPosition = targetPos; + targetForward = glm::normalize(targetFwd); + + // Calculate the desired camera position + // Position the camera behind and above the character + glm::vec3 offset = -targetForward * followDistance; + offset.y = followHeight; + + desiredPosition = targetPosition + offset; + + // Smooth camera movement using exponential smoothing + position = glm::mix(position, desiredPosition, 1.0f - pow(followSmoothness, deltaTime * 60.0f)); + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + + // Recalculate right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Positions the camera behind the character based on the character's forward direction +2. Adds height to give a better view of the character and surroundings +3. Uses exponential smoothing to create natural camera movement +4. Always keeps the camera focused on the character + +==== Occlusion Avoidance + +One of the most challenging aspects of a third-person camera is handling occlusion - when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: + +[source,cpp] +---- +void ThirdPersonCamera::handleOcclusion(const Scene& scene) { + // Cast a ray from the target to the desired camera position + Ray ray; + ray.origin = targetPosition; + ray.direction = glm::normalize(desiredPosition - targetPosition); + + // Check for intersections with scene objects + RaycastHit hit; + if (scene.raycast(ray, hit, glm::length(desiredPosition - targetPosition))) { + // If there's an intersection, move the camera to the hit point + // minus a small offset to avoid clipping + float offsetDistance = 0.2f; + position = hit.point - (ray.direction * offsetDistance); + + // Ensure we don't get too close to the target + float currentDistance = glm::length(position - targetPosition); + if (currentDistance < minDistance) { + position = targetPosition + ray.direction * minDistance; + } + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); + } +} +---- + +This implementation: + +1. Casts a ray from the character to the desired camera position +2. If the ray hits an object, moves the camera to the hit point (with a small offset) +3. Ensures the camera doesn't get too close to the character +4. Updates the camera orientation to maintain focus on the character + +===== Performance Considerations for Occlusion Avoidance + +When implementing occlusion avoidance, be mindful of performance: + +* *Use simplified collision geometry*: For raycasting, use simpler collision shapes than your rendering geometry +* *Limit the frequency of occlusion checks*: You may not need to check every frame on slower devices +* *Consider spatial partitioning*: Use structures like octrees to accelerate raycasts by quickly eliminating objects that can't possibly intersect with the ray +* *Optimize for mobile platforms*: For performance-constrained devices, consider simplifying the occlusion algorithm or reducing its precision + +==== Implementing Orbit Controls + +Many third-person games allow the player to orbit the camera around the character. Here's how to implement this functionality: + +[source,cpp] +---- +void ThirdPersonCamera::orbit(float horizontalAngle, float verticalAngle) { + // Update yaw and pitch based on input + yaw += horizontalAngle; + pitch += verticalAngle; + + // Constrain pitch to avoid flipping + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + + // Calculate the new camera position based on spherical coordinates + float radius = followDistance; + float yawRad = glm::radians(yaw); + float pitchRad = glm::radians(pitch); + + // Convert spherical coordinates to Cartesian + glm::vec3 offset; + offset.x = radius * cos(yawRad) * cos(pitchRad); + offset.y = radius * sin(pitchRad); + offset.z = radius * sin(yawRad) * cos(pitchRad); + + // Set the desired position + desiredPosition = targetPosition + offset; + + // Update camera vectors + front = glm::normalize(targetPosition - desiredPosition); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Updates the camera's yaw and pitch based on user input +2. Constrains the pitch to prevent the camera from flipping +3. Calculates a new camera position using spherical coordinates +4. Keeps the camera focused on the character + +==== Integrating with Character Movement + +To create a complete third-person camera system, we need to integrate it with character movement. Here's an example of how to use the third-person camera in a game loop: + +[source,cpp] +---- +void gameLoop(float deltaTime) { + // Update character position and orientation based on input + character.update(deltaTime); + + // Update camera position to follow the character + thirdPersonCamera.updatePosition( + character.getPosition(), + character.getForward(), + deltaTime + ); + + // Handle camera occlusion + thirdPersonCamera.handleOcclusion(scene); + + // Process camera orbit input (if any) + if (mouseInputDetected) { + thirdPersonCamera.orbit(mouseDeltaX, mouseDeltaY); + } + + // Get the view and projection matrices for rendering + glm::mat4 viewMatrix = thirdPersonCamera.getViewMatrix(); + glm::mat4 projMatrix = thirdPersonCamera.getProjectionMatrix(aspectRatio); + + // Use these matrices for rendering the scene + renderer.render(scene, viewMatrix, projMatrix); +} +---- + +[NOTE] +==== +For more advanced camera techniques, refer to the Advanced Camera Techniques section in the Appendix. +==== + +In the next section, we'll integrate our camera system with Vulkan to render 3D scenes. + +link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc index 21bd479d..3f7b5000 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -11,6 +11,16 @@ == Integrating Camera with Vulkan +=== Libraries Used in This Tutorial + +Before we dive into the integration, let's briefly introduce the key libraries we'll be using: + +* *GLFW* (Graphics Library Framework): A lightweight, multi-platform library for creating windows, contexts, and surfaces, handling input, and events. We use it for window management and input handling. [https://www.glfw.org/] + +* *GLM* (OpenGL Mathematics): A mathematics library for graphics programming that provides vector and matrix operations similar to GLSL. We use it for all our 3D math operations. [https://github.com/g-truc/glm] + +* *Vulkan*: The low-level graphics API we're using for rendering. [https://www.vulkan.org/] + Now that we have a camera system and understand transformation matrices, let's integrate them with our Vulkan application. We'll focus on how to set up uniform buffers for our matrices and update them each frame based on camera movement. === Uniform Buffer Setup From 8b83ead6aa6864f34176ffe47b40ec368acbd6b0 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 22:43:09 -0700 Subject: [PATCH 018/102] address the requested items --- .../05_rendering_pipeline.adoc | 4 ++- .../02_lighting_models.adoc | 2 ++ .../Loading_Models/02_project_setup.adoc | 28 +++++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index aebc8f3f..89b1b439 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -578,7 +578,9 @@ Dynamic rendering offers several advantages over traditional render passes: 4. *Reduced State Management*: Fewer objects to track and synchronize. 5. *Easier Debugging*: Simpler rendering code is easier to debug and maintain. -With dynamic rendering, we specify all rendering state (render targets, load/store operations, etc.) directly in the vkCmdBeginRendering call, rather than setting it up ahead of time in a VkRenderPass object. This allows for more dynamic rendering workflows and simplifies the implementation of techniques like deferred rendering. +With dynamic rendering, we specify all rendering states (render targets, +load/store operations, etc.) directly within the vkCmdBeginRendering call, +rather than setting it up ahead of time in a VkRenderPass object. This allows for more dynamic rendering workflows and simplifies the implementation of techniques like deferred rendering. ===== Dynamic Rendering in Rendergraphs diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc index ecc3e2a2..2e9d57e7 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc @@ -105,6 +105,8 @@ The key principles of PBR include: * *Fresnel Effect*: Reflectivity changes with viewing angle * *Metallic-Roughness Workflow*: Materials are defined by their base color, metalness, and roughness +Considerations for using PBR: + * *Advantages*: Realistic results that remain consistent across different lighting conditions, intuitive parameters for artists * *Disadvantages*: More complex and computationally expensive * *When to use*: For modern games and applications where realism is important diff --git a/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc b/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc index 5e7b79b6..3f9efd9a 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc @@ -22,19 +22,37 @@ When designing an asset organization system, consider these key principles: 3. *Discoverability* - Make assets easy to find and reference 4. *Scalability* - Design for growth as your project expands -Here's an example of how assets might be organized in a final product: +Here's an example of how assets might be organized in a final product, demonstrating all four principles: [source] ---- assets/ - ├── models/ // 3D model files - │ ├── characters/ // Character models + ├── models/ // 3D model files (Categorization) + │ ├── characters/ // Character models (Hierarchy) + │ │ ├── player/ // Player character models (Hierarchy) + │ │ └── npc/ // Non-player character models (Hierarchy) │ ├── environments/ // Environment models + │ │ ├── indoor/ // Indoor environment models + │ │ └── outdoor/ // Outdoor environment models │ └── props/ // Prop models - └── shaders/ // Shader files + ├── textures/ // Texture files (Categorization) + │ ├── common/ // Shared textures (Discoverability) + │ └── high_resolution/ // High-res textures for close-up views (Scalability) + ├── shaders/ // Shader files + │ ├── core/ // Essential shaders (Discoverability) + │ ├── effects/ // Special effect shaders + │ └── mobile/ // Mobile-optimized shaders (Scalability) + └── config/ // Configuration files + └── quality_presets/ // Different quality settings (Scalability) ---- -This example demonstrates categorization (models vs. shaders) and hierarchy (character models vs. environment models). The specific organization should be tailored to your project's needs, but the underlying principles remain consistent across different engines. +This example demonstrates all four principles: +- *Categorization*: Assets are grouped by type (models, textures, shaders, config) +- *Hierarchy*: Assets are organized in a nested structure (e.g., models > characters > player) +- *Discoverability*: Common assets are placed in dedicated folders (e.g., common textures, core shaders) making them easy to find +- *Scalability*: The structure accommodates different quality levels and platform-specific assets (e.g., high-resolution textures, mobile shaders, quality presets) + +The specific organization should be tailored to your project's needs, but the underlying principles remain consistent across different engines. === Asset Pipeline Concepts From 9b2343a0dbc2ef988062e145508be3e2b9e032b0 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 22:50:13 -0700 Subject: [PATCH 019/102] address the requested items --- en/Building_a_Simple_Engine/GUI/01_introduction.adoc | 2 +- en/Building_a_Simple_Engine/GUI/03_input_handling.adoc | 2 +- .../Loading_Models/04_loading_gltf.adoc | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc index d0d627e0..77fe9a54 100644 --- a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc @@ -13,7 +13,7 @@ Welcome to the "GUI" chapter of our "Building a Simple Engine" series! After implementing a camera system in the previous chapter, we'll now focus on adding a graphical user interface (GUI) to our Vulkan application. A well-designed GUI is essential for creating interactive applications that allow users to control settings, display information, and interact with the 3D scene. -In this chapter, we'll integrate a popular immediate-mode GUI library called Dear ImGui with our Vulkan engine. ImGui is widely used in the game and graphics industry due to its simplicity, performance, and flexibility. It allows developers to quickly create debug interfaces, tools, and in-game menus without the complexity of traditional retained-mode GUI systems. +In this chapter, we'll integrate a popular immediate-mode GUI library called Dear ImGui with our Vulkan engine. Dear ImGui is widely used in the game and graphics industry due to its simplicity, performance, and flexibility. It allows developers to quickly create debug interfaces, tools, and in-game menus without the complexity of traditional retained-mode GUI systems. In this chapter, we'll focus on: diff --git a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc index db4243f7..e367a9a1 100644 --- a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc +++ b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc @@ -13,7 +13,7 @@ One of the challenges when integrating a GUI into a 3D application is managing input events. We need to ensure that input events are correctly routed to either the GUI or the 3D scene. For example, if the user is interacting with a UI element, we don't want their mouse movements to also rotate the camera. -In this section, we'll explore how to handle input for both the GUI and the 3D scene, ensuring a smooth user experience regardless of the windowing system you choose to use. +In this section, we'll explore how to handle input for both the GUI and the 3D scene, ensuring a smooth user experience regardless of the windowing library you choose to use. [NOTE] ==== diff --git a/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc index ba63435a..def51751 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc @@ -129,6 +129,11 @@ glTF comes in two formats, each with its own advantages: === Understanding Physically Based Rendering (PBR) Materials +[NOTE] +==== +This section provides a brief overview of PBR materials as they relate to glTF loading. For a more comprehensive explanation of PBR concepts and lighting models, please refer to the link:../../Lighting_Materials/02_lighting_models.adoc#physically-based-rendering-pbr[Physically Based Rendering section] in the Lighting Materials chapter. +==== + Materials define how surfaces look when rendered. Modern games and engines use Physically Based Rendering (PBR), which simulates how light interacts with real-world materials based on physical principles. ==== The Evolution of Material Systems From 7734f214452d12810128528f69a076683ad9daa5 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 23 Jul 2025 15:54:43 -0700 Subject: [PATCH 020/102] Engine now builds and runs. IMGUI having issues. --- attachments/simple_engine/engine.cpp | 8 +++---- attachments/simple_engine/imgui_system.cpp | 20 ++++++++-------- attachments/simple_engine/imgui_system.h | 2 +- attachments/simple_engine/physics_system.cpp | 12 ++++++++++ attachments/simple_engine/renderer.h | 10 +++++--- attachments/simple_engine/renderer_core.cpp | 23 +++++++++++++++++++ .../simple_engine/renderer_rendering.cpp | 9 +++++++- 7 files changed, 65 insertions(+), 19 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 39cb6f2c..6952d6a2 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -261,6 +261,7 @@ void Engine::Update(float deltaTime) { } void Engine::Render() { + // Check if we have an active camera if (!activeCamera) { return; @@ -274,11 +275,8 @@ void Engine::Render() { } } - // Render the scene - renderer->Render(activeEntities, activeCamera); - - // Render ImGui - imguiSystem->Render(renderer->GetCurrentCommandBuffer()); + // Render the scene (ImGui will be rendered within the render pass) + renderer->Render(activeEntities, activeCamera, imguiSystem.get()); } float Engine::CalculateDeltaTime() { diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index b0039a14..7b9b2379 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -104,11 +104,12 @@ void ImGuiSystem::NewFrame() { ImGui::End(); } -void ImGuiSystem::Render(vk::CommandBuffer commandBuffer) { +void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer) { if (!initialized) { return; } + // End the frame and prepare for rendering ImGui::Render(); @@ -137,18 +138,19 @@ void ImGuiSystem::Render(vk::CommandBuffer commandBuffer) { struct PushConstBlock { float scale[2]; float translate[2]; - } pushConstBlock; + }; + std::array pushConstBlock; - pushConstBlock.scale[0] = 2.0f / ImGui::GetIO().DisplaySize.x; - pushConstBlock.scale[1] = 2.0f / ImGui::GetIO().DisplaySize.y; - pushConstBlock.translate[0] = -1.0f; - pushConstBlock.translate[1] = -1.0f; + pushConstBlock[0].scale[0] = 2.0f / ImGui::GetIO().DisplaySize.x; + pushConstBlock[0].scale[1] = 2.0f / ImGui::GetIO().DisplaySize.y; + pushConstBlock[0].translate[0] = -1.0f; + pushConstBlock[0].translate[1] = -1.0f; - commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(PushConstBlock), &pushConstBlock); + commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, pushConstBlock); // Bind vertex and index buffers - std::array vertexBuffers = {*vertexBuffer}; - std::array offsets = {0}; + std::array vertexBuffers = {*vertexBuffer}; + std::array offsets = {}; commandBuffer.bindVertexBuffers(0, vertexBuffers, offsets); commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint16); diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index 1c99ab35..49b41d65 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -55,7 +55,7 @@ class ImGuiSystem { * @brief Render the ImGui frame. * @param commandBuffer The command buffer to record rendering commands to. */ - void Render(vk::CommandBuffer commandBuffer); + void Render(vk::raii::CommandBuffer & commandBuffer); /** * @brief Handle mouse input. diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index ef2d3a14..031a7df9 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -965,6 +965,9 @@ void PhysicsSystem::UpdateGPUPhysicsData() { return; } + // TODO: Add validity checks for Vulkan resources if needed + // Temporarily removed to focus on main validation error investigation + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); @@ -1027,6 +1030,9 @@ void PhysicsSystem::ReadbackGPUPhysicsData() { return; } + // TODO: Add validity checks for Vulkan resources if needed + // Temporarily removed to focus on main validation error investigation + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); @@ -1058,12 +1064,18 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { return; } + // TODO: Add validity checks for Vulkan resources if needed + // Temporarily removed to focus on main validation error investigation + const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); // Update physics data on the GPU UpdateGPUPhysicsData(); + // Reset command buffer before beginning (required for reuse) + vulkanResources.commandBuffer.reset(); + // Begin command buffer vk::CommandBufferBeginInfo beginInfo; beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index cc461910..0d3291a4 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -17,6 +17,9 @@ import vulkan_hpp; #include "mesh_component.h" #include "camera_component.h" +// Forward declarations +class ImGuiSystem; + /** * @brief Structure for Vulkan queue family indices. */ @@ -98,8 +101,9 @@ class Renderer { * @brief Render the scene. * @param entities The entities to render. * @param camera The camera to use for rendering. + * @param imguiSystem The ImGui system for UI rendering (optional). */ - void Render(const std::vector& entities, CameraComponent* camera); + void Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem = nullptr); /** * @brief Wait for the device to be idle. @@ -199,8 +203,8 @@ class Renderer { * @brief Get the current command buffer. * @return The current command buffer. */ - vk::CommandBuffer GetCurrentCommandBuffer() const { - return *commandBuffers[currentFrame]; + vk::raii::CommandBuffer& GetCurrentCommandBuffer() { + return commandBuffers[currentFrame]; } /** diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index decf5fc0..e0de9362 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -169,46 +169,69 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer // Clean up renderer resources void Renderer::Cleanup() { if (initialized) { + std::cout << "Starting renderer cleanup..." << std::endl; + // Wait for the device to be idle before cleaning up device.waitIdle(); + std::cout << "Device idle, starting RAII object destruction..." << std::endl; // Clean up swap chain + std::cout << "Cleaning up swap chain..." << std::endl; cleanupSwapChain(); // Clear resources - RAII will handle destruction + std::cout << "Destroying sync objects..." << std::endl; imageAvailableSemaphores.clear(); renderFinishedSemaphores.clear(); inFlightFences.clear(); + + std::cout << "Destroying command buffers and pool..." << std::endl; commandBuffers.clear(); commandPool = nullptr; + + std::cout << "Destroying descriptor pool..." << std::endl; descriptorPool = nullptr; + + std::cout << "Destroying pipelines..." << std::endl; pbrGraphicsPipeline = nullptr; pbrPipelineLayout = nullptr; lightingPipeline = nullptr; lightingPipelineLayout = nullptr; graphicsPipeline = nullptr; pipelineLayout = nullptr; + + std::cout << "Destroying compute pipeline..." << std::endl; computePipeline = nullptr; computePipelineLayout = nullptr; computeDescriptorSetLayout = nullptr; computeDescriptorPool = nullptr; + + std::cout << "Destroying descriptor set layout..." << std::endl; descriptorSetLayout = nullptr; // Clear mesh resources - RAII will handle destruction + std::cout << "Destroying mesh resources..." << std::endl; meshResources.clear(); // Clear texture resources - RAII will handle destruction + std::cout << "Destroying texture resources..." << std::endl; textureResources.clear(); // Clear entity resources - RAII will handle destruction + std::cout << "Destroying entity resources..." << std::endl; entityResources.clear(); // Clear device, surface, debug messenger, and instance - RAII will handle destruction + std::cout << "Destroying device..." << std::endl; device = nullptr; + std::cout << "Destroying surface..." << std::endl; surface = nullptr; + std::cout << "Destroying debug messenger..." << std::endl; debugMessenger = nullptr; + std::cout << "Destroying instance..." << std::endl; instance = nullptr; + std::cout << "Renderer cleanup completed." << std::endl; initialized = false; } } diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 39504656..36432660 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -1,4 +1,5 @@ #include "renderer.h" +#include "imgui_system.h" #include #include #include @@ -290,7 +291,8 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera } // Render the scene -void Renderer::Render(const std::vector& entities, CameraComponent* camera) { +void Renderer::Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { + // Wait for the previous frame to finish device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); @@ -399,6 +401,11 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, 1, 0, 0, 0); } + // Render ImGui if provided + if (imguiSystem) { + imguiSystem->Render(commandBuffers[currentFrame]); + } + // End dynamic rendering commandBuffers[currentFrame].endRendering(); From b8a9b3979ca058614e7d917f89ca9b43e88ac56b Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 23 Jul 2025 16:10:31 -0700 Subject: [PATCH 021/102] Engine now renders. Bunch of validation errors and it looks like imgui needs some help. it also looks like cleanup logic and resizing needs work. --- attachments/simple_engine/imgui_system.cpp | 9 +-- attachments/simple_engine/imgui_system.h | 2 +- attachments/simple_engine/renderer_core.cpp | 58 ------------------- .../simple_engine/renderer_rendering.cpp | 4 +- .../simple_engine/renderer_resources.cpp | 13 ++++- attachments/simple_engine/renderer_utils.cpp | 4 +- 6 files changed, 22 insertions(+), 68 deletions(-) diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 7b9b2379..d13b884b 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -173,7 +173,7 @@ void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer) { commandBuffer.setScissor(0, {scissor}); // Bind descriptor set (font texture) - commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {descriptorSet}, {}); + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {*descriptorSet}, {}); // Draw commandBuffer.drawIndexed(pcmd->ElemCount, 1, indexOffset, vertexOffset, 0); @@ -451,8 +451,9 @@ bool ImGuiSystem::createDescriptorSet() { allocInfo.pSetLayouts = &(*descriptorSetLayout); const vk::raii::Device& device = renderer->GetRaiiDevice(); - auto descriptorSets = device.allocateDescriptorSets(allocInfo); - descriptorSet = descriptorSets[0]; // Store the first (and only) descriptor set + vk::raii::DescriptorSets descriptorSets(device, allocInfo); + descriptorSet = std::move(descriptorSets[0]); // Store the first (and only) descriptor set + std::cout << "ImGui created descriptor set with handle: " << *descriptorSet << std::endl; // Update descriptor set vk::DescriptorImageInfo imageInfo; @@ -461,7 +462,7 @@ bool ImGuiSystem::createDescriptorSet() { imageInfo.sampler = *fontSampler; vk::WriteDescriptorSet writeSet; - writeSet.dstSet = descriptorSet; + writeSet.dstSet = *descriptorSet; writeSet.descriptorCount = 1; writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; writeSet.pImageInfo = &imageInfo; diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index 49b41d65..ca42e6d5 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -107,7 +107,7 @@ class ImGuiSystem { // Vulkan resources vk::raii::DescriptorPool descriptorPool = nullptr; vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; - vk::DescriptorSet descriptorSet = nullptr; + vk::raii::DescriptorSet descriptorSet = nullptr; vk::raii::PipelineLayout pipelineLayout = nullptr; vk::raii::Pipeline pipeline = nullptr; vk::raii::Sampler fontSampler = nullptr; diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index e0de9362..f5e14a04 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -173,64 +173,6 @@ void Renderer::Cleanup() { // Wait for the device to be idle before cleaning up device.waitIdle(); - std::cout << "Device idle, starting RAII object destruction..." << std::endl; - - // Clean up swap chain - std::cout << "Cleaning up swap chain..." << std::endl; - cleanupSwapChain(); - - // Clear resources - RAII will handle destruction - std::cout << "Destroying sync objects..." << std::endl; - imageAvailableSemaphores.clear(); - renderFinishedSemaphores.clear(); - inFlightFences.clear(); - - std::cout << "Destroying command buffers and pool..." << std::endl; - commandBuffers.clear(); - commandPool = nullptr; - - std::cout << "Destroying descriptor pool..." << std::endl; - descriptorPool = nullptr; - - std::cout << "Destroying pipelines..." << std::endl; - pbrGraphicsPipeline = nullptr; - pbrPipelineLayout = nullptr; - lightingPipeline = nullptr; - lightingPipelineLayout = nullptr; - graphicsPipeline = nullptr; - pipelineLayout = nullptr; - - std::cout << "Destroying compute pipeline..." << std::endl; - computePipeline = nullptr; - computePipelineLayout = nullptr; - computeDescriptorSetLayout = nullptr; - computeDescriptorPool = nullptr; - - std::cout << "Destroying descriptor set layout..." << std::endl; - descriptorSetLayout = nullptr; - - // Clear mesh resources - RAII will handle destruction - std::cout << "Destroying mesh resources..." << std::endl; - meshResources.clear(); - - // Clear texture resources - RAII will handle destruction - std::cout << "Destroying texture resources..." << std::endl; - textureResources.clear(); - - // Clear entity resources - RAII will handle destruction - std::cout << "Destroying entity resources..." << std::endl; - entityResources.clear(); - - // Clear device, surface, debug messenger, and instance - RAII will handle destruction - std::cout << "Destroying device..." << std::endl; - device = nullptr; - std::cout << "Destroying surface..." << std::endl; - surface = nullptr; - std::cout << "Destroying debug messenger..." << std::endl; - debugMessenger = nullptr; - std::cout << "Destroying instance..." << std::endl; - instance = nullptr; - std::cout << "Renderer cleanup completed." << std::endl; initialized = false; } diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 36432660..839b936e 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -394,8 +394,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Bind the index buffer commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); - // Bind the descriptor set - commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {entityIt->second.descriptorSets[currentFrame]}, {}); + // Bind the descriptor set (dereference RAII wrapper) + commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {*entityIt->second.descriptorSets[currentFrame]}, {}); // Draw the mesh commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, 1, 0, 0, 0); diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 23c845a9..c0ed384c 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -437,7 +437,7 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa } } - // Create descriptor sets + // Create descriptor sets using RAII std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, @@ -445,7 +445,16 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .pSetLayouts = layouts.data() }; - entityIt->second.descriptorSets = device.allocateDescriptorSets(allocInfo); + // Allocate descriptor sets using RAII wrapper + vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); + + // Convert to vector of individual RAII descriptor sets + entityIt->second.descriptorSets.clear(); + entityIt->second.descriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + entityIt->second.descriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); + std::cout << "Created descriptor set " << i << " with handle: " << *entityIt->second.descriptorSets[i] << std::endl; + } // Update descriptor sets for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp index c0f91b4d..9e4101fc 100644 --- a/attachments/simple_engine/renderer_utils.cpp +++ b/attachments/simple_engine/renderer_utils.cpp @@ -49,11 +49,13 @@ vk::Format Renderer::findSupportedFormat(const std::vector& candidat // Find depth format vk::Format Renderer::findDepthFormat() { - return findSupportedFormat( + vk::Format depthFormat = findSupportedFormat( {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, vk::ImageTiling::eOptimal, vk::FormatFeatureFlagBits::eDepthStencilAttachment ); + std::cout << "Found depth format: " << static_cast(depthFormat) << std::endl; + return depthFormat; } // Check if format has stencil component From e773353ec7b374f30872e230cf5a2aeee70dca11 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 23 Jul 2025 17:30:41 -0700 Subject: [PATCH 022/102] address comments. --- .../Engine_Architecture/02_architectural_patterns.adoc | 10 +++++----- .../Engine_Architecture/04_resource_management.adoc | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index 309ba11e..9d33745a 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -19,7 +19,7 @@ Before diving into specific patterns, it's important to clarify that while we're Here's a brief introduction to the most common architectural patterns used in game and rendering engines: -==== Layered Architecture +==== link:https://games-1312234642.cos.ap-guangzhou.myqcloud.com/course/GAMES104/GAMES104_Lecture02.pdf[Layered Architecture] Layered architecture divides the system into distinct layers, each with a specific responsibility. Typical layers include platform abstraction, resource management, rendering, scene management, and application layers. @@ -32,9 +32,9 @@ image::../../../images/layered_architecture_diagram.png[Layered Architecture Dia For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#layered-architecture[Appendix: Layered Architecture]. -==== Data-Oriented Design +==== link:https://www.youtube.com/watch?v=rX0ItVEVjHc[Data-Oriented Design] -Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. It emphasizes cache-friendly memory layouts and bulk processing of data. +Data-Oriented Design (DOD) focuses on organizing data for efficient processing rather than organizing code around objects. It emphasizes cache-friendly memory layouts and bulk processing of data. image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=400] @@ -45,7 +45,7 @@ image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Dia For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#data-oriented-design[Appendix: Data-Oriented Design]. -==== Service Locator Pattern +==== link:https://gameprogrammingpatterns.com/service-locator.html[Service Locator Pattern] The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. @@ -58,7 +58,7 @@ image::../../../images/service_locator_pattern_diagram.svg[Service Locator Patte For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#service-locator-pattern[Appendix: Service Locator Pattern]. -=== Component-Based Architecture +=== link:https://gameprogrammingpatterns.com/component.html[Component-Based Architecture] Component-based architecture is widely used in modern game engines and forms the foundation of our Vulkan rendering engine. It promotes composition over inheritance and allows for more flexible entity design. diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc index 6ac3a5c8..6cf07e65 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc @@ -21,7 +21,7 @@ When designing a resource management system, you'll need to address several chal 2. *Caching* - Frequently used resources should be cached to avoid redundant loading. 3. *Reference Counting* - Track how many objects are using a resource to know when it can be safely unloaded. 4. *Hot Reloading* - Allow resources to be updated while the application is running (useful during development). -5. *Streaming* - Load resources asynchronously to avoid blocking the main thread. +5. *Streaming* - Load resources asynchronously to avoid blocking the main thread. It's good to realize that "streaming" here is meant in terms of sending data from one location to another in chunks. It's the same type of algorithm that might be familiar in networking or internet downloading, however, it only differs in the sense that it relates to transferring data between the system memory and the GPU memory. 6. *Memory Management* - Efficiently allocate and deallocate memory for resources. === Resource Handles From 6256f77c4f6044d4140cff69016c875bf768fbb3 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 23 Jul 2025 17:37:04 -0700 Subject: [PATCH 023/102] address comments. --- .../Engine_Architecture/05_rendering_pipeline.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 89b1b439..e9b09266 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -459,6 +459,14 @@ Proper synchronization is crucial in Vulkan because: 2. *Parallel Execution*: Modern GPUs execute commands in parallel, which can lead to race conditions without proper synchronization. 3. *Memory Visibility*: Changes made by one operation may not be visible to another without proper barriers. +The vulkan tutorial includes a more detailed discussion of synchronization, the proper uses of the primitives described above. + +* link:../../03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc[Synchronization] +* link:../../03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc[Frames In Flight] +* link:../../11_Compute_Shader.adoc[Compute Shader] +* link:../../17_Multithreading.adoc[Multithreading] + + ===== Pipeline Barriers Pipeline barriers are one of the most important synchronization primitives in Vulkan. They ensure that operations before the barrier complete before operations after the barrier begin, and they can also perform layout transitions for images. From e6b8583d2da83c07297a232c5bedc549b6e7e4b6 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 23 Jul 2025 19:28:07 -0700 Subject: [PATCH 024/102] It runs now, IMGUI is happier. --- attachments/simple_engine/renderer.h | 13 ++- .../simple_engine/renderer_pipelines.cpp | 61 +++++++++++--- .../simple_engine/renderer_rendering.cpp | 80 +++++++++++++++++-- attachments/simple_engine/renderer_utils.cpp | 20 +++-- 4 files changed, 144 insertions(+), 30 deletions(-) diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 0d3291a4..062c1b5a 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -258,6 +258,11 @@ class Renderer { vk::raii::PipelineLayout lightingPipelineLayout = nullptr; vk::raii::Pipeline lightingPipeline = nullptr; + // Pipeline rendering create info structures (for proper lifetime management) + vk::PipelineRenderingCreateInfo mainPipelineRenderingCreateInfo; + vk::PipelineRenderingCreateInfo pbrPipelineRenderingCreateInfo; + vk::PipelineRenderingCreateInfo lightingPipelineRenderingCreateInfo; + // Compute pipeline vk::raii::PipelineLayout computePipelineLayout = nullptr; vk::raii::Pipeline computePipeline = nullptr; @@ -279,10 +284,9 @@ class Renderer { vk::raii::DeviceMemory depthImageMemory = nullptr; vk::raii::ImageView depthImageView = nullptr; - // Descriptor set layout and pool + // Descriptor set layouts (declared before pools and sets) vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; - vk::raii::DescriptorPool descriptorPool = nullptr; // Mesh resources struct MeshResources { @@ -306,7 +310,7 @@ class Renderer { // Default texture resources (used when no texture is provided) TextureResources defaultTextureResources; - // Entity resources + // Entity resources (contains descriptor sets - must be declared before descriptor pool) struct EntityResources { std::vector uniformBuffers; std::vector uniformBuffersMemory; @@ -315,6 +319,9 @@ class Renderer { }; std::unordered_map entityResources; + // Descriptor pool (declared after entity resources to ensure proper destruction order) + vk::raii::DescriptorPool descriptorPool = nullptr; + // Current frame index uint32_t currentFrame = 0; diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index 493e7725..5c3a2275 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -223,15 +223,24 @@ bool Renderer::createGraphicsPipeline() { pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); // Create pipeline rendering info - vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + vk::Format depthFormat = findDepthFormat(); + std::cout << "Creating main graphics pipeline with depth format: " << static_cast(depthFormat) << std::endl; + + // Initialize member variable for proper lifetime management + mainPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, - .depthAttachmentFormat = findDepthFormat() + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = vk::Format::eUndefined }; // Create graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo{ - .pNext = &pipelineRenderingCreateInfo, + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &mainPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, .stageCount = 2, .pStages = shaderStages, .pVertexInputState = &vertexInputInfo, @@ -242,7 +251,11 @@ bool Renderer::createGraphicsPipeline() { .pDepthStencilState = &depthStencil, .pColorBlendState = &colorBlending, .pDynamicState = &dynamicState, - .layout = *pipelineLayout + .layout = *pipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 }; graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); @@ -411,15 +424,23 @@ bool Renderer::createPBRPipeline() { pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); // Create pipeline rendering info - vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + vk::Format depthFormat = findDepthFormat(); + + // Initialize member variable for proper lifetime management + pbrPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, - .depthAttachmentFormat = findDepthFormat() + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = vk::Format::eUndefined }; // Create graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo{ - .pNext = &pipelineRenderingCreateInfo, + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, .stageCount = 2, .pStages = shaderStages, .pVertexInputState = &vertexInputInfo, @@ -430,7 +451,11 @@ bool Renderer::createPBRPipeline() { .pDepthStencilState = &depthStencil, .pColorBlendState = &colorBlending, .pDynamicState = &dynamicState, - .layout = *pbrPipelineLayout + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 }; pbrGraphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); @@ -565,15 +590,23 @@ bool Renderer::createLightingPipeline() { lightingPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); // Create pipeline rendering info - vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + vk::Format depthFormat = findDepthFormat(); + + // Initialize member variable for proper lifetime management + lightingPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, - .depthAttachmentFormat = findDepthFormat() + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = vk::Format::eUndefined }; // Create graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo{ - .pNext = &pipelineRenderingCreateInfo, + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &lightingPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, .stageCount = 2, .pStages = shaderStages, .pVertexInputState = &vertexInputInfo, @@ -584,7 +617,11 @@ bool Renderer::createLightingPipeline() { .pDepthStencilState = &depthStencil, .pColorBlendState = &colorBlending, .pDynamicState = &dynamicState, - .layout = *lightingPipelineLayout + .layout = *lightingPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 }; lightingPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 839b936e..49cdd714 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -204,8 +204,12 @@ bool Renderer::createSyncObjects() { renderFinishedSemaphores.clear(); inFlightFences.clear(); - imageAvailableSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); - renderFinishedSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + // Create semaphores per swapchain image to avoid reuse issues + size_t swapchainImageCount = swapChainImages.size(); + imageAvailableSemaphores.reserve(swapchainImageCount); + renderFinishedSemaphores.reserve(swapchainImageCount); + + // Keep fences per frame in flight for CPU-GPU synchronization inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); // Create semaphore and fence info @@ -214,10 +218,14 @@ bool Renderer::createSyncObjects() { .flags = vk::FenceCreateFlagBits::eSignaled }; - // Create semaphores and fences for each frame in flight - for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + // Create semaphores for each swapchain image + for (size_t i = 0; i < swapchainImageCount; i++) { imageAvailableSemaphores.emplace_back(device, semaphoreInfo); renderFinishedSemaphores.emplace_back(device, semaphoreInfo); + } + + // Create fences for each frame in flight + for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { inFlightFences.emplace_back(device, fenceInfo); } @@ -298,7 +306,9 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Acquire the next image from the swap chain uint32_t imageIndex; - auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); + // Use currentFrame for semaphore indexing to ensure consistency + uint32_t semaphoreIndex = currentFrame % imageAvailableSemaphores.size(); + auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[semaphoreIndex]); imageIndex = result.second; // Check if the swap chain needs to be recreated @@ -326,6 +336,33 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Update rendering area renderingInfo.setRenderArea(vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + // Transition swapchain image layout for rendering + vk::ImageMemoryBarrier renderBarrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlags{}, + {}, + {}, + renderBarrier + ); + // Begin dynamic rendering with vk::raii commandBuffers[currentFrame].beginRendering(renderingInfo); @@ -409,6 +446,33 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // End dynamic rendering commandBuffers[currentFrame].endRendering(); + // Transition swapchain image layout for presentation + vk::ImageMemoryBarrier imageBarrier{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .dstAccessMask = vk::AccessFlagBits::eNone, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eBottomOfPipe, + vk::DependencyFlags{}, + {}, + {}, + imageBarrier + ); + // End command buffer commandBuffers[currentFrame].end(); @@ -416,12 +480,12 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; vk::SubmitInfo submitInfo{ .waitSemaphoreCount = 1, - .pWaitSemaphores = &*imageAvailableSemaphores[currentFrame], + .pWaitSemaphores = &*imageAvailableSemaphores[semaphoreIndex], .pWaitDstStageMask = waitStages, .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, - .pSignalSemaphores = &*renderFinishedSemaphores[currentFrame] + .pSignalSemaphores = &*renderFinishedSemaphores[semaphoreIndex] }; graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); @@ -429,7 +493,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Present the image vk::PresentInfoKHR presentInfo{ .waitSemaphoreCount = 1, - .pWaitSemaphores = &*renderFinishedSemaphores[currentFrame], + .pWaitSemaphores = &*renderFinishedSemaphores[semaphoreIndex], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp index 9e4101fc..5d452c55 100644 --- a/attachments/simple_engine/renderer_utils.cpp +++ b/attachments/simple_engine/renderer_utils.cpp @@ -49,13 +49,19 @@ vk::Format Renderer::findSupportedFormat(const std::vector& candidat // Find depth format vk::Format Renderer::findDepthFormat() { - vk::Format depthFormat = findSupportedFormat( - {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, - vk::ImageTiling::eOptimal, - vk::FormatFeatureFlagBits::eDepthStencilAttachment - ); - std::cout << "Found depth format: " << static_cast(depthFormat) << std::endl; - return depthFormat; + try { + vk::Format depthFormat = findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment + ); + std::cout << "Found depth format: " << static_cast(depthFormat) << std::endl; + return depthFormat; + } catch (const std::exception& e) { + std::cerr << "Failed to find supported depth format, falling back to D32_SFLOAT: " << e.what() << std::endl; + // Fallback to D32_SFLOAT which is widely supported + return vk::Format::eD32Sfloat; + } } // Check if format has stencil component From e3f6fc64e2f2bb244efc58a7f5f4191258b7d973 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 23 Jul 2025 20:32:56 -0700 Subject: [PATCH 025/102] everything runs and can close without validation warnings other than the semaphore. resizing still doesn't work. Next step is to get the parts of the engine to demonstrate things. --- attachments/simple_engine/engine.cpp | 10 ++++++ attachments/simple_engine/imgui_system.cpp | 17 ++-------- attachments/simple_engine/renderer.h | 10 +++++- attachments/simple_engine/renderer_core.cpp | 9 ++++++ .../simple_engine/renderer_rendering.cpp | 31 ++++++++++++++++++- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 6952d6a2..87790a16 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -299,6 +299,16 @@ void Engine::HandleResize(int width, int height) { if (activeCamera) { activeCamera->SetAspectRatio(static_cast(width) / static_cast(height)); } + + // Notify the renderer that the framebuffer has been resized + if (renderer) { + renderer->SetFramebufferResized(); + } + + // Notify ImGui system about the resize + if (imguiSystem) { + imguiSystem->HandleResize(static_cast(width), static_cast(height)); + } } #if PLATFORM_ANDROID diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index d13b884b..7a0b78f4 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -63,21 +63,6 @@ void ImGuiSystem::Cleanup() { if (renderer) { renderer->WaitIdle(); } - - // Clean up Vulkan resources - using RAII, these will be automatically destroyed - pipeline = nullptr; - pipelineLayout = nullptr; - descriptorSetLayout = nullptr; - fontSampler = nullptr; - fontView = nullptr; - fontImage = nullptr; - fontMemory = nullptr; - vertexBuffer = nullptr; - vertexBufferMemory = nullptr; - indexBuffer = nullptr; - indexBufferMemory = nullptr; - descriptorPool = nullptr; - // Destroy ImGui context if (context) { ImGui::DestroyContext(context); @@ -612,11 +597,13 @@ bool ImGuiSystem::createPipeline() { dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); + vk::Format depthFormat = renderer->findDepthFormat(); // Create the graphics pipeline with dynamic rendering vk::PipelineRenderingCreateInfo renderingInfo; renderingInfo.colorAttachmentCount = 1; vk::Format colorFormat = renderer->GetSwapChainImageFormat(); // Get the actual swapchain format renderingInfo.pColorAttachmentFormats = &colorFormat; + renderingInfo.depthAttachmentFormat = depthFormat; vk::GraphicsPipelineCreateInfo pipelineInfo; pipelineInfo.stageCount = static_cast(shaderStages.size()); diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 062c1b5a..36cb4101 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -215,6 +215,15 @@ class Renderer { return swapChainImageFormat; } + /** + * @brief Set the framebuffer resized flag. + * This should be called when the window is resized to trigger swap chain recreation. + */ + void SetFramebufferResized() { + framebufferResized = true; + } + vk::Format findDepthFormat(); + private: // Platform Platform* platform = nullptr; @@ -415,7 +424,6 @@ class Renderer { vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags); vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features); - vk::Format findDepthFormat(); bool hasStencilComponent(vk::Format format); std::vector readFile(const std::string& filename); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index f5e14a04..1d8769b5 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -3,6 +3,7 @@ #include #include #include +#include VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file @@ -173,6 +174,14 @@ void Renderer::Cleanup() { // Wait for the device to be idle before cleaning up device.waitIdle(); + for (auto& resources : entityResources | std::views::values) { + for (auto& memory : resources.uniformBuffersMemory) { + memory.unmapMemory(); + } + resources.descriptorSets.clear(); + resources.uniformBuffers.clear(); + resources.uniformBuffersMemory.clear(); + } std::cout << "Renderer cleanup completed." << std::endl; initialized = false; } diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 49cdd714..6cdb5a7e 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -1,5 +1,6 @@ #include "renderer.h" #include "imgui_system.h" +#include "imgui/imgui.h" #include #include #include @@ -246,6 +247,23 @@ void Renderer::cleanupSwapChain() { // Clean up swap chain image views swapChainImageViews.clear(); + // Clean up descriptor pool (this will automatically clean up descriptor sets) + descriptorPool = nullptr; + + // Clean up pipelines + graphicsPipeline = nullptr; + pbrGraphicsPipeline = nullptr; + lightingPipeline = nullptr; + + // Clean up pipeline layouts + pipelineLayout = nullptr; + pbrPipelineLayout = nullptr; + lightingPipelineLayout = nullptr; + + // Clean up sync objects (they need to be recreated with new swap chain image count) + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + // Clean up swap chain swapChain = nullptr; } @@ -258,10 +276,15 @@ void Renderer::recreateSwapChain() { // Clean up old swap chain resources cleanupSwapChain(); - // Recreate swap chain + // Recreate swap chain and related resources createSwapChain(); createImageViews(); createDepthResources(); + + // Recreate sync objects with correct sizing for new swap chain + createSyncObjects(); + + // Recreate descriptor pool and pipelines createDescriptorPool(); createGraphicsPipeline(); createPBRPipeline(); @@ -314,6 +337,12 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Check if the swap chain needs to be recreated if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { framebufferResized = false; + + // If ImGui has started a frame, we need to end it properly before returning + if (imguiSystem) { + ImGui::EndFrame(); + } + recreateSwapChain(); return; } else if (result.first != vk::Result::eSuccess) { From 4a3b450c64d496678e2d9578393a66361e0c2667 Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 24 Jul 2025 00:18:19 -0700 Subject: [PATCH 026/102] everything runs and can close without validation warnings other than the semaphore. resizing still doesn't work. Next step is to get the parts of the engine to demonstrate things. --- .../simple_engine/Assets/grass-step-right.wav | Bin 0 -> 72828 bytes attachments/simple_engine/CMakeLists.txt | 2 + attachments/simple_engine/audio_system.cpp | 851 +++++++++++++++++- attachments/simple_engine/audio_system.h | 75 ++ attachments/simple_engine/engine.cpp | 29 +- attachments/simple_engine/engine.h | 2 +- attachments/simple_engine/imgui_system.cpp | 138 ++- attachments/simple_engine/imgui_system.h | 18 + 8 files changed, 1070 insertions(+), 45 deletions(-) create mode 100644 attachments/simple_engine/Assets/grass-step-right.wav diff --git a/attachments/simple_engine/Assets/grass-step-right.wav b/attachments/simple_engine/Assets/grass-step-right.wav new file mode 100644 index 0000000000000000000000000000000000000000..7e1ee8ea38cf1174de5bcf9a6e1f43357c586a49 GIT binary patch literal 72828 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 P5jUuf?6>EA@umMC{h7!z literal 0 HcmV?d00001 diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index c1972eae..53cc5cc7 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -15,6 +15,7 @@ find_package (Vulkan REQUIRED) find_package (tinygltf REQUIRED) find_package (KTX REQUIRED) find_package (stb REQUIRED) +find_package (OpenAL REQUIRED) # set up Vulkan C++ module add_library(VulkanCppModule) @@ -124,6 +125,7 @@ target_link_libraries(SimpleEngine PRIVATE tinygltf::tinygltf KTX::ktx stb::stb + OpenAL::OpenAL ) if(NOT DEFINED ANDROID) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 54d335d0..84bf725f 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -2,10 +2,53 @@ #include #include +#include #include +#include +#include +#include +#include + +// OpenAL headers +#ifdef __APPLE__ +#include +#include +#else +#include +#include +#endif #include "renderer.h" +// OpenAL error checking utility +static void CheckOpenALError(const std::string& operation) { + ALenum error = alGetError(); + if (error != AL_NO_ERROR) { + std::cerr << "OpenAL Error in " << operation << ": "; + switch (error) { + case AL_INVALID_NAME: + std::cerr << "AL_INVALID_NAME"; + break; + case AL_INVALID_ENUM: + std::cerr << "AL_INVALID_ENUM"; + break; + case AL_INVALID_VALUE: + std::cerr << "AL_INVALID_VALUE"; + break; + case AL_INVALID_OPERATION: + std::cerr << "AL_INVALID_OPERATION"; + break; + case AL_OUT_OF_MEMORY: + std::cerr << "AL_OUT_OF_MEMORY"; + break; + default: + std::cerr << "Unknown error " << error; + break; + } + std::cerr << std::endl; + } +} + // Concrete implementation of AudioSource class ConcreteAudioSource : public AudioSource { public: @@ -14,6 +57,9 @@ class ConcreteAudioSource : public AudioSource { void Play() override { playing = true; + playbackPosition = 0; + delayTimer = 0.0f; + inDelayPhase = false; std::cout << "Playing audio source: " << name << std::endl; } @@ -24,6 +70,9 @@ class ConcreteAudioSource : public AudioSource { void Stop() override { playing = false; + playbackPosition = 0; + delayTimer = 0.0f; + inDelayPhase = false; std::cout << "Stopping audio source: " << name << std::endl; } @@ -55,6 +104,61 @@ class ConcreteAudioSource : public AudioSource { return playing; } + // Additional methods for delay functionality + void SetAudioLength(uint32_t lengthInSamples) { + audioLengthSamples = lengthInSamples; + } + + void UpdatePlayback(float deltaTime, uint32_t samplesProcessed) { + if (!playing) return; + + if (inDelayPhase) { + // We're in the delay phase between playthroughs + delayTimer += deltaTime; + if (delayTimer >= delayDuration) { + // Delay finished, restart playback + inDelayPhase = false; + playbackPosition = 0; + delayTimer = 0.0f; + std::cout << "Delay finished, restarting audio playback for: " << name << std::endl; + } + } else { + // Normal playback, update position + playbackPosition += samplesProcessed; + + // Check if we've reached the end of the audio + if (audioLengthSamples > 0 && playbackPosition >= audioLengthSamples) { + if (loop) { + // Start delay phase before looping + inDelayPhase = true; + delayTimer = 0.0f; + std::cout << "Audio finished, starting 1.5s delay before loop for: " << name << std::endl; + } else { + // Stop playing if not looping + playing = false; + playbackPosition = 0; + std::cout << "Audio finished, stopping playback for: " << name << std::endl; + } + } + } + } + + [[nodiscard]] bool ShouldProcessAudio() const { + return playing && !inDelayPhase; + } + + [[nodiscard]] uint32_t GetPlaybackPosition() const { + return playbackPosition; + } + + [[nodiscard]] const std::string& GetName() const { + return name; + } + + [[nodiscard]] const float* GetPosition() const { + return position; + } + private: std::string name; bool playing = false; @@ -62,9 +166,329 @@ class ConcreteAudioSource : public AudioSource { float volume = 1.0f; float position[3] = {0.0f, 0.0f, 0.0f}; float velocity[3] = {0.0f, 0.0f, 0.0f}; + + // Delay and timing functionality + uint32_t playbackPosition = 0; // Current position in samples + uint32_t audioLengthSamples = 0; // Total length of audio in samples + float delayTimer = 0.0f; // Timer for delay between loops + bool inDelayPhase = false; // Whether we're currently in delay phase + static constexpr float delayDuration = 1.5f; // 1.5 second delay between loops +}; + +// OpenAL audio output device implementation +class OpenALAudioOutputDevice : public AudioOutputDevice { +public: + OpenALAudioOutputDevice() = default; + ~OpenALAudioOutputDevice() override { + Stop(); + Cleanup(); + } + + bool Initialize(uint32_t sampleRate, uint32_t channels, uint32_t bufferSize) override { + this->sampleRate = sampleRate; + this->channels = channels; + this->bufferSize = bufferSize; + + std::cout << "Initializing OpenAL audio output device: " << sampleRate << "Hz, " + << channels << " channels, buffer size: " << bufferSize << std::endl; + + // Initialize OpenAL + device = alcOpenDevice(nullptr); // Use default device + if (!device) { + std::cerr << "Failed to open OpenAL device" << std::endl; + return false; + } + + context = alcCreateContext(device, nullptr); + if (!context) { + std::cerr << "Failed to create OpenAL context" << std::endl; + alcCloseDevice(device); + device = nullptr; + return false; + } + + if (!alcMakeContextCurrent(context)) { + std::cerr << "Failed to make OpenAL context current" << std::endl; + alcDestroyContext(context); + alcCloseDevice(device); + context = nullptr; + device = nullptr; + return false; + } + + // Generate OpenAL source + alGenSources(1, &source); + CheckOpenALError("alGenSources"); + + // Generate OpenAL buffers for streaming + alGenBuffers(NUM_BUFFERS, buffers); + CheckOpenALError("alGenBuffers"); + + // Set source properties + alSourcef(source, AL_PITCH, 1.0f); + alSourcef(source, AL_GAIN, 1.0f); + alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + alSourcei(source, AL_LOOPING, AL_FALSE); + CheckOpenALError("Source setup"); + + // Initialize audio buffer + audioBuffer.resize(bufferSize * channels); + + // Initialize buffer tracking + queuedBufferCount = 0; + while (!availableBuffers.empty()) { + availableBuffers.pop(); + } + + initialized = true; + std::cout << "OpenAL audio output device initialized successfully" << std::endl; + return true; + } + + bool Start() override { + if (!initialized) { + std::cerr << "OpenAL audio output device not initialized" << std::endl; + return false; + } + + if (playing) { + return true; // Already playing + } + + playing = true; + + // Start audio playback thread + audioThread = std::thread(&OpenALAudioOutputDevice::AudioThreadFunction, this); + + std::cout << "OpenAL audio output device started" << std::endl; + return true; + } + + bool Stop() override { + if (!playing) { + return true; // Already stopped + } + + playing = false; + + // Wait for audio thread to finish + if (audioThread.joinable()) { + audioThread.join(); + } + + // Stop OpenAL source + if (initialized && source != 0) { + alSourceStop(source); + CheckOpenALError("alSourceStop"); + } + + std::cout << "OpenAL audio output device stopped" << std::endl; + return true; + } + + bool WriteAudio(const float* data, uint32_t sampleCount) override { + if (!initialized || !playing) { + return false; + } + + std::lock_guard lock(bufferMutex); + + // Add audio data to the queue + for (uint32_t i = 0; i < sampleCount * channels; i++) { + audioQueue.push(data[i]); + } + + return true; + } + + bool IsPlaying() const override { + return playing; + } + + uint32_t GetPosition() const override { + return playbackPosition; + } + +private: + static const int NUM_BUFFERS = 4; + + uint32_t sampleRate = 44100; + uint32_t channels = 2; + uint32_t bufferSize = 1024; + bool initialized = false; + bool playing = false; + uint32_t playbackPosition = 0; + + // OpenAL objects + ALCdevice* device = nullptr; + ALCcontext* context = nullptr; + ALuint source = 0; + ALuint buffers[NUM_BUFFERS]; + int currentBuffer = 0; + + std::vector audioBuffer; + std::queue audioQueue; + std::mutex bufferMutex; + std::thread audioThread; + + // Buffer management for OpenAL streaming + std::queue availableBuffers; + int queuedBufferCount = 0; + + void Cleanup() { + if (initialized) { + // Clean up OpenAL resources + if (source != 0) { + alDeleteSources(1, &source); + source = 0; + } + + alDeleteBuffers(NUM_BUFFERS, buffers); + + if (context) { + alcMakeContextCurrent(nullptr); + alcDestroyContext(context); + context = nullptr; + } + + if (device) { + alcCloseDevice(device); + device = nullptr; + } + + // Reset buffer tracking + queuedBufferCount = 0; + while (!availableBuffers.empty()) { + availableBuffers.pop(); + } + + initialized = false; + } + } + + void AudioThreadFunction() { + std::cout << "OpenAL audio playback thread started" << std::endl; + + // Calculate sleep time for audio buffer updates (in milliseconds) + const auto sleepTime = std::chrono::milliseconds( + static_cast((bufferSize * 1000) / sampleRate / 8) // Eighth buffer time for responsiveness + ); + + while (playing) { + ProcessAudioBuffer(); + std::this_thread::sleep_for(sleepTime); + } + + std::cout << "OpenAL audio playback thread stopped" << std::endl; + } + + void ProcessAudioBuffer() { + std::lock_guard lock(bufferMutex); + + // Fill audio buffer from queue + uint32_t samplesProcessed = 0; + const uint32_t maxSamples = bufferSize * channels; + + for (uint32_t i = 0; i < maxSamples && !audioQueue.empty(); i++) { + audioBuffer[i] = audioQueue.front(); + audioQueue.pop(); + samplesProcessed++; + } + + if (samplesProcessed > 0) { + // Convert float samples to 16-bit PCM for OpenAL + std::vector pcmBuffer(samplesProcessed); + for (uint32_t i = 0; i < samplesProcessed; i++) { + // Clamp and convert to 16-bit PCM + float sample = std::max(-1.0f, std::min(1.0f, audioBuffer[i])); + pcmBuffer[i] = static_cast(sample * 32767.0f); + } + + // Check for processed buffers and unqueue them + ALint processed = 0; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + CheckOpenALError("alGetSourcei AL_BUFFERS_PROCESSED"); + + // Unqueue processed buffers and add them to available buffers + while (processed > 0) { + ALuint buffer; + alSourceUnqueueBuffers(source, 1, &buffer); + CheckOpenALError("alSourceUnqueueBuffers"); + + // Add the unqueued buffer to available buffers + availableBuffers.push(buffer); + processed--; + } + + // Only proceed if we have an available buffer + ALuint buffer = 0; + if (!availableBuffers.empty()) { + buffer = availableBuffers.front(); + availableBuffers.pop(); + } else if (queuedBufferCount < NUM_BUFFERS) { + // Use a buffer that hasn't been queued yet + buffer = buffers[queuedBufferCount]; + } else { + // No available buffers, skip this frame + return; + } + + // Validate buffer parameters + if (samplesProcessed == 0 || pcmBuffer.empty()) { + // Re-add buffer to available list if we can't use it + if (queuedBufferCount >= NUM_BUFFERS) { + availableBuffers.push(buffer); + } + return; + } + + // Determine format based on channels + ALenum format = (channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; + + // Upload audio data to OpenAL buffer + alBufferData(buffer, format, pcmBuffer.data(), + samplesProcessed * sizeof(int16_t), sampleRate); + CheckOpenALError("alBufferData"); + + // Queue the buffer + alSourceQueueBuffers(source, 1, &buffer); + CheckOpenALError("alSourceQueueBuffers"); + + // Track that we've queued this buffer + if (queuedBufferCount < NUM_BUFFERS) { + queuedBufferCount++; + } + + // Start playing if not already playing + ALint sourceState; + alGetSourcei(source, AL_SOURCE_STATE, &sourceState); + CheckOpenALError("alGetSourcei AL_SOURCE_STATE"); + + if (sourceState != AL_PLAYING) { + alSourcePlay(source); + CheckOpenALError("alSourcePlay"); + } + + playbackPosition += samplesProcessed / channels; + + // For debugging: print audio activity + static uint32_t debugCounter = 0; + if (++debugCounter % 100 == 0) { // Print every 100 buffer updates + std::cout << "OpenAL output: processed " << samplesProcessed + << " samples, position: " << playbackPosition << std::endl; + } + } + } }; AudioSystem::~AudioSystem() { + // Stop and clean up audio output device + if (outputDevice) { + outputDevice->Stop(); + outputDevice.reset(); + } + // Destructor implementation sources.clear(); audioData.clear(); @@ -73,40 +497,279 @@ AudioSystem::~AudioSystem() { cleanupHRTFBuffers(); } +void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition) { + const float sampleRate = 44100.0f; + const float frequency = 1000.0f; // 1000Hz ping - louder and more penetrating frequency + const float pingDuration = 0.5f; // 0.5 second ping duration + const uint32_t pingSamples = static_cast(pingDuration * sampleRate); + const float silenceDuration = 1.0f; // 1 second silence after ping + const uint32_t silenceSamples = static_cast(silenceDuration * sampleRate); + const uint32_t totalCycleSamples = pingSamples + silenceSamples; + + for (uint32_t i = 0; i < sampleCount; i++) { + uint32_t globalPosition = playbackPosition + i; + uint32_t cyclePosition = globalPosition % totalCycleSamples; + + if (cyclePosition < pingSamples) { + // Generate ping with envelope + float t = static_cast(cyclePosition) / sampleRate; + float pingProgress = static_cast(cyclePosition) / static_cast(pingSamples); + + // Create envelope: quick attack, sustain, exponential decay + float envelope; + if (pingProgress < 0.1f) { + // Attack phase (first 10% of ping) + envelope = pingProgress / 0.1f; + } else if (pingProgress < 0.3f) { + // Sustain phase (20% of ping at full volume) + envelope = 1.0f; + } else { + // Decay phase (remaining 70% with exponential decay) + float decayProgress = (pingProgress - 0.3f) / 0.7f; + envelope = std::exp(-decayProgress * 5.0f); // Exponential decay + } + + // Generate sine wave with envelope + float sineWave = std::sin(2.0f * M_PI * frequency * t); + buffer[i] = 0.8f * envelope * sineWave; // 0.8 amplitude for much louder, clearly audible sound + } else { + // Silence phase + buffer[i] = 0.0f; + } + } +} + bool AudioSystem::Initialize(Renderer* renderer) { - // This is a placeholder implementation - // In a real implementation, this would initialize the audio API (e.g., OpenAL) + if (renderer) { + std::cout << "Initializing HRTF audio system with Vulkan compute shader support" << std::endl; + + // Validate renderer if provided + if (!renderer->IsInitialized()) { + std::cerr << "AudioSystem::Initialize: Renderer is not initialized" << std::endl; + return false; + } + + // Store the renderer for compute shader support + this->renderer = renderer; + } else { + std::cout << "Initializing HRTF audio system with CPU-based processing (no renderer)" << std::endl; + this->renderer = nullptr; + } + + // Generate default HRTF data for spatial audio processing + LoadHRTFData(""); // Pass empty filename to force generation of default HRTF data + std::cout << "Using generated HRTF data for spatial audio processing" << std::endl; - std::cout << "Initializing audio system" << std::endl; + // Enable HRTF processing by default for 3D spatial audio + EnableHRTF(true); - // Store the renderer for compute shader support - this->renderer = renderer; + // Set default listener properties + SetListenerPosition(0.0f, 0.0f, 0.0f); + SetListenerOrientation(0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); + SetListenerVelocity(0.0f, 0.0f, 0.0f); + SetMasterVolume(1.0f); + + // Initialize audio output device + outputDevice = std::make_unique(); + if (!outputDevice->Initialize(44100, 2, 1024)) { + std::cerr << "Failed to initialize audio output device" << std::endl; + return false; + } + + // Start audio output + if (!outputDevice->Start()) { + std::cerr << "Failed to start audio output device" << std::endl; + return false; + } initialized = true; + std::cout << "HRTF audio system initialized successfully with audio output" << std::endl; return true; } void AudioSystem::Update(float deltaTime) { - // This is a placeholder implementation - // In a real implementation, this would update the audio system + if (!initialized) { + return; + } + + // Check if we have a valid renderer for Vulkan compute shader processing + bool hasValidRenderer = (renderer && renderer->IsInitialized()); + + // Update audio sources and process spatial audio + for (auto& source : sources) { + if (!source->IsPlaying()) { + continue; + } + + // Cast to ConcreteAudioSource to access timing methods + ConcreteAudioSource* concreteSource = static_cast(source.get()); + + // Update playback timing and delay logic + concreteSource->UpdatePlayback(deltaTime, 0); // Will update with actual samples processed later + + // Only process audio if not in delay phase + if (!concreteSource->ShouldProcessAudio()) { + continue; + } + + // Process audio with HRTF spatial processing (works with or without renderer) + if (hrtfEnabled && !hrtfData.empty()) { + // Get source position for spatial processing + const float* sourcePosition = concreteSource->GetPosition(); + + // Create sample buffers for processing + const uint32_t sampleCount = 1024; + std::vector inputBuffer(sampleCount, 0.0f); + std::vector outputBuffer(sampleCount * 2, 0.0f); + uint32_t actualSamplesProcessed = 0; + + // Generate audio signal from loaded audio data + auto audioIt = audioData.find(concreteSource->GetName()); + if (audioIt != audioData.end() && !audioIt->second.empty()) { + // Use actual loaded audio data with proper position tracking + const auto& data = audioIt->second; + uint32_t playbackPos = concreteSource->GetPlaybackPosition(); + + for (uint32_t i = 0; i < sampleCount; i++) { + uint32_t dataIndex = (playbackPos + i) * 4; // 4 bytes per sample (16-bit stereo) + + if (dataIndex + 1 < data.size()) { + // Convert from 16-bit PCM to float + int16_t sample = *reinterpret_cast(&data[dataIndex]); + inputBuffer[i] = static_cast(sample) / 32768.0f; + actualSamplesProcessed++; + } else { + // Reached end of audio data + inputBuffer[i] = 0.0f; + } + } + } else { + // Generate sine wave ping for debugging + GenerateSineWavePing(inputBuffer.data(), sampleCount, concreteSource->GetPlaybackPosition()); + actualSamplesProcessed = sampleCount; + } + + // Process audio with HRTF spatial processing using Vulkan compute shader + ProcessHRTF(inputBuffer.data(), outputBuffer.data(), sampleCount, sourcePosition); - // Update listener position, orientation, and velocity based on camera + // Send processed audio to output device + if (outputDevice && outputDevice->IsPlaying()) { + // Apply master volume + for (uint32_t i = 0; i < sampleCount * 2; i++) { + outputBuffer[i] *= masterVolume; + } - // Update audio sources + // Send to audio output device + if (!outputDevice->WriteAudio(outputBuffer.data(), sampleCount)) { + std::cerr << "Failed to write audio data to output device" << std::endl; + } + } + + // Update playback timing with actual samples processed + concreteSource->UpdatePlayback(0.0f, actualSamplesProcessed); + } + } + + // Apply master volume changes to all active sources for (auto& source : sources) { - // Update source properties + if (source->IsPlaying()) { + // Master volume is applied during HRTF processing and individual source volume control + // Volume scaling is handled in the ProcessHRTF function + } + } + + // Clean up finished audio sources + sources.erase( + std::remove_if(sources.begin(), sources.end(), + [](const std::unique_ptr& source) { + // Keep all sources active for continuous playback + // Audio sources can be stopped/started via their Play/Stop methods + return false; + }), + sources.end() + ); + + // Update timing for audio processing with low-latency chunks + static float accumulatedTime = 0.0f; + accumulatedTime += deltaTime; + + // Process audio in 20ms chunks for optimal latency + const float audioChunkTime = 0.02f; // 20ms chunks for real-time audio + if (accumulatedTime >= audioChunkTime) { + // Trigger audio buffer updates for smooth playback + // The HRTF processing ensures spatial audio is updated continuously + accumulatedTime = 0.0f; + + // Update listener properties if they have changed + // This ensures spatial audio positioning stays current with camera movement } } bool AudioSystem::LoadAudio(const std::string& filename, const std::string& name) { - // This is a placeholder implementation - // In a real implementation, this would load the audio file - std::cout << "Loading audio file: " << filename << " as " << name << std::endl; - // Simulate loading audio data - std::vector data(1024, 0); // Dummy data - audioData[name] = data; + // Open the WAV file + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + std::cerr << "Failed to open audio file: " << filename << std::endl; + return false; + } + + // Read WAV header + struct WAVHeader { + char riff[4]; // "RIFF" + uint32_t fileSize; // File size - 8 + char wave[4]; // "WAVE" + char fmt[4]; // "fmt " + uint32_t fmtSize; // Format chunk size + uint16_t audioFormat; // Audio format (1 = PCM) + uint16_t numChannels; // Number of channels + uint32_t sampleRate; // Sample rate + uint32_t byteRate; // Byte rate + uint16_t blockAlign; // Block align + uint16_t bitsPerSample; // Bits per sample + char data[4]; // "data" + uint32_t dataSize; // Data size + }; + + WAVHeader header; + file.read(reinterpret_cast(&header), sizeof(WAVHeader)); + + // Validate WAV header + if (std::strncmp(header.riff, "RIFF", 4) != 0 || + std::strncmp(header.wave, "WAVE", 4) != 0 || + std::strncmp(header.fmt, "fmt ", 4) != 0 || + std::strncmp(header.data, "data", 4) != 0) { + std::cerr << "Invalid WAV file format: " << filename << std::endl; + file.close(); + return false; + } + + // Only support PCM format for now + if (header.audioFormat != 1) { + std::cerr << "Unsupported audio format (only PCM supported): " << filename << std::endl; + file.close(); + return false; + } + + // Read audio data + std::vector data(header.dataSize); + file.read(reinterpret_cast(data.data()), header.dataSize); + file.close(); + + if (file.gcount() != static_cast(header.dataSize)) { + std::cerr << "Failed to read complete audio data from: " << filename << std::endl; + return false; + } + + // Store the audio data + audioData[name] = std::move(data); + + std::cout << "Successfully loaded WAV file: " << filename + << " (Channels: " << header.numChannels + << ", Sample Rate: " << header.sampleRate + << ", Bits: " << header.bitsPerSample + << ", Size: " << header.dataSize << " bytes)" << std::endl; return true; } @@ -122,6 +785,20 @@ AudioSource* AudioSystem::CreateAudioSource(const std::string& name) { // Create a new audio source auto source = std::make_unique(name); + // Calculate audio length in samples for timing + const auto& data = it->second; + if (!data.empty()) { + // Assuming 16-bit stereo audio at 44.1kHz (standard WAV format) + // The audio data reading uses dataIndex = (playbackPos + i) * 4 + // So we need to calculate length based on how many individual samples we can read + // Each 4 bytes represents one stereo sample pair, so total individual samples = data.size() / 4 + uint32_t totalSamples = static_cast(data.size()) / 4; + + // Set the audio length for proper timing + source->SetAudioLength(totalSamples); + std::cout << "Set audio length for " << name << ": " << totalSamples << " samples (corrected for 4-byte indexing)" << std::endl; + } + // Store the source AudioSource* sourcePtr = source.get(); sources.push_back(std::move(source)); @@ -130,6 +807,30 @@ AudioSource* AudioSystem::CreateAudioSource(const std::string& name) { return sourcePtr; } +AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { + std::cout << "Creating debug ping audio source: " << name << std::endl; + + // Create a new audio source for debugging + auto source = std::make_unique(name); + + // Set up debug ping parameters + // The ping will cycle every 1.5 seconds (0.5s ping + 1.0s silence) + const float sampleRate = 44100.0f; + const float pingDuration = 0.5f; + const float silenceDuration = 1.0f; + const uint32_t totalCycleSamples = static_cast((pingDuration + silenceDuration) * sampleRate); + + // Set the audio length for proper timing (infinite loop for debugging) + source->SetAudioLength(totalCycleSamples); + + // Store the source + AudioSource* sourcePtr = source.get(); + sources.push_back(std::move(source)); + + std::cout << "Debug ping audio source created: " << name << " (800Hz ping every 1.5 seconds)" << std::endl; + return sourcePtr; +} + void AudioSystem::SetListenerPosition(float x, float y, float z) { listenerPosition[0] = x; listenerPosition[1] = y; @@ -175,30 +876,115 @@ bool AudioSystem::IsHRTFEnabled() const { } bool AudioSystem::LoadHRTFData(const std::string& filename) { - // This is a placeholder implementation - // In a real implementation, this would load HRTF data from a file - - std::cout << "Loading HRTF data from: " << filename << std::endl; - - // Simulate loading HRTF data - // In a real implementation, this would parse a file containing HRTF impulse responses + if (!filename.empty()) { + std::cout << "Loading HRTF data from: " << filename << std::endl; + } else { + std::cout << "Generating default HRTF data (no file specified)" << std::endl; + } - // Create some dummy HRTF data for testing - // Typically, HRTF data consists of impulse responses for different directions + // HRTF parameters const uint32_t hrtfSampleCount = 256; // Number of samples per impulse response const uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) const uint32_t channelCount = 2; // Stereo (left and right ears) + const float sampleRate = 44100.0f; // Sample rate for HRTF data + const float speedOfSound = 343.0f; // Speed of sound in m/s + const float headRadius = 0.0875f; // Average head radius in meters + + // Try to load from file first (only if filename is provided) + if (!filename.empty()) { + std::ifstream file(filename, std::ios::binary); + if (file.is_open()) { + // Read file header to determine format + char header[4]; + file.read(header, 4); + + if (std::strncmp(header, "HRTF", 4) == 0) { + // Custom HRTF format + uint32_t fileHrtfSize, filePositionCount, fileChannelCount; + file.read(reinterpret_cast(&fileHrtfSize), sizeof(uint32_t)); + file.read(reinterpret_cast(&filePositionCount), sizeof(uint32_t)); + file.read(reinterpret_cast(&fileChannelCount), sizeof(uint32_t)); + + if (fileChannelCount == channelCount) { + hrtfData.resize(fileHrtfSize * filePositionCount * fileChannelCount); + file.read(reinterpret_cast(hrtfData.data()), hrtfData.size() * sizeof(float)); + + hrtfSize = fileHrtfSize; + numHrtfPositions = filePositionCount; + + file.close(); + std::cout << "Successfully loaded HRTF data from file" << std::endl; + return true; + } + } + file.close(); + } + } + + // Generate realistic HRTF data based on acoustic modeling + std::cout << "Generating realistic HRTF impulse responses..." << std::endl; // Resize the HRTF data vector hrtfData.resize(hrtfSampleCount * positionCount * channelCount); - // Fill with dummy data (simple exponential decay) + // Generate HRTF impulse responses for each position for (uint32_t pos = 0; pos < positionCount; pos++) { + // Calculate azimuth and elevation for this position + uint32_t azimuthIndex = pos % 36; + uint32_t elevationIndex = pos / 36; + + float azimuth = (static_cast(azimuthIndex) * 10.0f - 180.0f) * M_PI / 180.0f; + float elevation = (static_cast(elevationIndex) * 15.0f - 90.0f) * M_PI / 180.0f; + + // Convert to Cartesian coordinates + float x = std::cos(elevation) * std::sin(azimuth); + float y = std::sin(elevation); + float z = std::cos(elevation) * std::cos(azimuth); + for (uint32_t channel = 0; channel < channelCount; channel++) { + // Calculate ear position (left ear: -0.1m, right ear: +0.1m on x-axis) + float earX = (channel == 0) ? -0.1f : 0.1f; + + // Calculate distance from source to ear + float dx = x - earX; + float dy = y; + float dz = z; + float distance = std::sqrt(dx * dx + dy * dy + dz * dz); + + // Calculate time delay (ITD - Interaural Time Difference) + float timeDelay = distance / speedOfSound; + uint32_t sampleDelay = static_cast(timeDelay * sampleRate); + + // Calculate head shadow effect (ILD - Interaural Level Difference) + float shadowFactor = 1.0f; + if (channel == 0 && azimuth > 0) { // Left ear, source on right + shadowFactor = 0.3f + 0.7f * std::exp(-azimuth * 2.0f); + } else if (channel == 1 && azimuth < 0) { // Right ear, source on left + shadowFactor = 0.3f + 0.7f * std::exp(azimuth * 2.0f); + } + + // Generate impulse response for (uint32_t i = 0; i < hrtfSampleCount; i++) { - float value = std::exp(-static_cast(i) / 20.0f) * 0.5f; - // Add some variation based on position and channel - value *= (1.0f + 0.2f * std::sin(pos * 0.1f + channel * 3.14159f)); + float value = 0.0f; + + // Direct path impulse + if (i >= sampleDelay && i < sampleDelay + 10) { + float t = static_cast(i - sampleDelay) / sampleRate; + value = shadowFactor * std::exp(-t * 1000.0f) * std::cos(2.0f * M_PI * 1000.0f * t); + } + + // Early reflections (simplified) + for (int refl = 1; refl <= 3; refl++) { + uint32_t reflDelay = sampleDelay + refl * 20; + if (i >= reflDelay && i < reflDelay + 5) { + float t = static_cast(i - reflDelay) / sampleRate; + float reflGain = shadowFactor * 0.3f / static_cast(refl); + value += reflGain * std::exp(-t * 2000.0f) * std::cos(2.0f * M_PI * 800.0f * t); + } + } + + // Apply distance attenuation + value /= std::max(1.0f, distance); uint32_t index = pos * hrtfSampleCount * channelCount + channel * hrtfSampleCount + i; hrtfData[index] = value; @@ -210,6 +996,7 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { hrtfSize = hrtfSampleCount; numHrtfPositions = positionCount; + std::cout << "Successfully generated " << positionCount << " HRTF impulse responses" << std::endl; return true; } @@ -259,9 +1046,8 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin memcpy(data, ¶ms, sizeof(HRTFParams)); paramsBufferMemory.unmapMemory(); - // Dispatch compute shader - // In a real implementation, this would use a compute shader to perform HRTF convolution - // For now, we'll simulate the HRTF processing on the CPU + // Perform HRTF processing using CPU-based convolution + // This implementation provides real-time 3D audio spatialization // Calculate direction from listener to source float direction[3]; @@ -419,7 +1205,6 @@ bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { paramsBufferMemory = vk::raii::DeviceMemory(device, paramsAllocInfo); paramsBuffer.bindMemory(*paramsBufferMemory, 0); - std::cout << "HRTF buffers created successfully" << std::endl; return true; } catch (const std::exception& e) { diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index 437deb54..22eea43f 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -79,6 +79,63 @@ class AudioSource { // Forward declarations class Renderer; +/** + * @brief Interface for audio output devices. + */ +class AudioOutputDevice { +public: + /** + * @brief Default constructor. + */ + AudioOutputDevice() = default; + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~AudioOutputDevice() = default; + + /** + * @brief Initialize the audio output device. + * @param sampleRate The sample rate (e.g., 44100). + * @param channels The number of channels (typically 2 for stereo). + * @param bufferSize The buffer size in samples. + * @return True if initialization was successful, false otherwise. + */ + virtual bool Initialize(uint32_t sampleRate, uint32_t channels, uint32_t bufferSize) = 0; + + /** + * @brief Start audio playback. + * @return True if successful, false otherwise. + */ + virtual bool Start() = 0; + + /** + * @brief Stop audio playback. + * @return True if successful, false otherwise. + */ + virtual bool Stop() = 0; + + /** + * @brief Write audio data to the output device. + * @param data Pointer to the audio data (interleaved stereo float samples). + * @param sampleCount Number of samples per channel to write. + * @return True if successful, false otherwise. + */ + virtual bool WriteAudio(const float* data, uint32_t sampleCount) = 0; + + /** + * @brief Check if the device is currently playing. + * @return True if playing, false otherwise. + */ + virtual bool IsPlaying() const = 0; + + /** + * @brief Get the current playback position in samples. + * @return Current position in samples. + */ + virtual uint32_t GetPosition() const = 0; +}; + /** * @brief Class for managing audio. */ @@ -122,6 +179,13 @@ class AudioSystem { */ AudioSource* CreateAudioSource(const std::string& name); + /** + * @brief Create a sine wave ping audio source for debugging. + * @param name The name to assign to the debug audio source. + * @return Pointer to the created audio source, or nullptr if creation failed. + */ + AudioSource* CreateDebugPingSource(const std::string& name); + /** * @brief Set the listener position in 3D space. * @param x The x-coordinate. @@ -185,6 +249,14 @@ class AudioSystem { */ bool ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition); + /** + * @brief Generate a sine wave ping for debugging purposes. + * @param buffer The output buffer to fill with ping audio data. + * @param sampleCount The number of samples to generate. + * @param playbackPosition The current playback position for timing. + */ + void GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition); + private: // Loaded audio data std::unordered_map> audioData; @@ -212,6 +284,9 @@ class AudioSystem { // Renderer for compute shader support Renderer* renderer = nullptr; + // Audio output device for sending processed audio to speakers + std::unique_ptr outputDevice = nullptr; + // Vulkan resources for HRTF processing vk::raii::Buffer inputBuffer = nullptr; vk::raii::DeviceMemory inputBufferMemory = nullptr; diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 87790a16..34fea6bc 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -69,7 +69,7 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool } // Initialize audio system - if (!audioSystem->Initialize()) { + if (!audioSystem->Initialize(renderer.get())) { return false; } @@ -84,6 +84,9 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool return false; } + // Connect ImGui system to audio system for UI controls + imguiSystem->SetAudioSystem(audioSystem.get()); + initialized = true; return true; #endif @@ -281,17 +284,28 @@ void Engine::Render() { float Engine::CalculateDeltaTime() { // Get current time - auto currentTime = static_cast(std::chrono::duration_cast( + auto currentTime = static_cast(std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch() - ).count()) / 1000.0f; + ).count()); + + // Initialize lastFrameTime on first call + if (lastFrameTime == 0) { + lastFrameTime = currentTime; + return 0.016f; // Return ~16ms (60 FPS) for first frame + } // Calculate delta time - float delta = currentTime - lastFrameTime; + uint64_t delta = currentTime - lastFrameTime; // Update last frame time lastFrameTime = currentTime; - return delta; + // Clamp delta time to reasonable values (prevent huge jumps) + if (delta > 10) { // Cap at 100ms (10 FPS minimum) + delta = 16; // Use 16ms instead + } + + return delta / 1000.0f; // Convert to seconds } void Engine::HandleResize(int width, int height) { @@ -358,7 +372,7 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo } // Initialize audio system - if (!audioSystem->Initialize()) { + if (!audioSystem->Initialize(renderer.get())) { return false; } @@ -377,6 +391,9 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo return false; } + // Connect ImGui system to audio system for UI controls + imguiSystem->SetAudioSystem(audioSystem.get()); + initialized = true; return true; } diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index c60ef32e..b9dcba93 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -180,7 +180,7 @@ class Engine { // Delta time calculation float deltaTime = 0.0f; - float lastFrameTime = 0.0f; + uint64_t lastFrameTime = 0; /** * @brief Update the engine state. diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 7a0b78f4..16ec66cc 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -1,5 +1,6 @@ #include "imgui_system.h" #include "renderer.h" +#include "audio_system.h" // Include ImGui headers #include "imgui/imgui.h" @@ -72,6 +73,32 @@ void ImGuiSystem::Cleanup() { initialized = false; } +void ImGuiSystem::SetAudioSystem(AudioSystem* audioSystem) { + this->audioSystem = audioSystem; + + // Load the grass-step-right.wav file and create audio source + if (audioSystem) { + if (audioSystem->LoadAudio("../Assets/grass-step-right.wav", "grass_step")) { + audioSource = audioSystem->CreateAudioSource("grass_step"); + if (audioSource) { + audioSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + audioSource->SetVolume(0.8f); + audioSource->SetLoop(true); + std::cout << "Audio source created and configured for HRTF demo" << std::endl; + } + } + + // Also create a debug ping source for testing + debugPingSource = audioSystem->CreateDebugPingSource("debug_ping"); + if (debugPingSource) { + debugPingSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + debugPingSource->SetVolume(0.8f); + debugPingSource->SetLoop(true); + std::cout << "Debug ping source created for audio debugging" << std::endl; + } + } +} + void ImGuiSystem::NewFrame() { if (!initialized) { return; @@ -79,13 +106,114 @@ void ImGuiSystem::NewFrame() { ImGui::NewFrame(); - // Create your UI elements here - // For example: - ImGui::Begin("Simple Engine Demo"); + // Create HRTF Audio Control UI + ImGui::Begin("HRTF Audio Controls"); ImGui::Text("Hello, Vulkan!"); - if (ImGui::Button("Click me!")) { - // Handle button click + ImGui::Text("3D Audio Position Control"); + + // Audio source selection + ImGui::Separator(); + ImGui::Text("Audio Source Selection:"); + + static bool useDebugPing = false; + if (ImGui::Checkbox("Use Debug Ping (800Hz sine wave)", &useDebugPing)) { + // Stop current audio + if (audioSource && audioSource->IsPlaying()) { + audioSource->Stop(); + } + if (debugPingSource && debugPingSource->IsPlaying()) { + debugPingSource->Stop(); + } + std::cout << "Switched to " << (useDebugPing ? "debug ping" : "file audio") << " source" << std::endl; } + + // Display current audio source position + ImGui::Text("Audio Source Position: (%.2f, %.2f, %.2f)", audioSourceX, audioSourceY, audioSourceZ); + ImGui::Text("Current Source: %s", useDebugPing ? "Debug Ping (800Hz)" : "grass-step-right.wav"); + + // Directional control buttons + ImGui::Separator(); + ImGui::Text("Directional Controls:"); + + // Get current active source + AudioSource* currentSource = useDebugPing ? debugPingSource : audioSource; + + // Up button + if (ImGui::Button("Up")) { + audioSourceY += 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved up to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + + // Left and Right buttons on same line + if (ImGui::Button("Left")) { + audioSourceX -= 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved left to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("Right")) { + audioSourceX += 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved right to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + + // Down button + if (ImGui::Button("Down")) { + audioSourceY -= 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved down to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + + // Audio playback controls + ImGui::Separator(); + ImGui::Text("Playback Controls:"); + + // Play button + if (ImGui::Button("Play")) { + if (currentSource) { + currentSource->Play(); + if (useDebugPing) { + std::cout << "Started playing debug ping (800Hz sine wave) with HRTF processing" << std::endl; + } else { + std::cout << "Started playing grass-step-right.wav with HRTF processing" << std::endl; + } + } else { + std::cout << "No audio source available - audio system not initialized" << std::endl; + } + } + ImGui::SameLine(); + + // Stop button + if (ImGui::Button("Stop")) { + if (currentSource) { + currentSource->Stop(); + if (useDebugPing) { + std::cout << "Stopped debug ping playback" << std::endl; + } else { + std::cout << "Stopped audio playback" << std::endl; + } + } + } + + // Additional info + ImGui::Separator(); + if (audioSystem && audioSystem->IsHRTFEnabled()) { + ImGui::Text("HRTF Processing: ENABLED"); + ImGui::Text("Use directional buttons to move the audio source in 3D space"); + ImGui::Text("You should hear the audio move around you!"); + } else { + ImGui::Text("HRTF Processing: DISABLED"); + } + ImGui::End(); } diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index ca42e6d5..6610f57d 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -12,6 +12,8 @@ import vulkan_hpp; // Forward declarations class Renderer; +class AudioSystem; +class AudioSource; struct ImGuiContext; /** @@ -97,6 +99,12 @@ class ImGuiSystem { */ bool WantCaptureMouse() const; + /** + * @brief Set the audio system reference for audio controls. + * @param audioSystem Pointer to the audio system. + */ + void SetAudioSystem(AudioSystem* audioSystem); + private: // ImGui context ImGuiContext* context = nullptr; @@ -104,6 +112,16 @@ class ImGuiSystem { // Renderer reference Renderer* renderer = nullptr; + // Audio system reference + AudioSystem* audioSystem = nullptr; + AudioSource* audioSource = nullptr; + AudioSource* debugPingSource = nullptr; + + // Audio position tracking + float audioSourceX = 1.0f; + float audioSourceY = 0.0f; + float audioSourceZ = 0.0f; + // Vulkan resources vk::raii::DescriptorPool descriptorPool = nullptr; vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; From 08331666618fdec2fd0f2f28a8811098d16ba62f Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 24 Jul 2025 00:35:42 -0700 Subject: [PATCH 027/102] audio HRTF works well and debug switch to turn on CPU only mode vs GPU processing mode. --- attachments/simple_engine/audio_system.cpp | 61 +++++++++++++++++++++- attachments/simple_engine/audio_system.h | 13 +++++ attachments/simple_engine/imgui_system.cpp | 29 ++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 84bf725f..74914d9f 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -875,6 +875,15 @@ bool AudioSystem::IsHRTFEnabled() const { return hrtfEnabled; } +void AudioSystem::SetHRTFCPUOnly(bool cpuOnly) { + hrtfCPUOnly = cpuOnly; + std::cout << "HRTF processing mode set to " << (cpuOnly ? "CPU-only" : "Vulkan shader (when available)") << std::endl; +} + +bool AudioSystem::IsHRTFCPUOnly() const { + return hrtfCPUOnly; +} + bool AudioSystem::LoadHRTFData(const std::string& filename) { if (!filename.empty()) { std::cout << "Loading HRTF data from: " << filename << std::endl; @@ -1001,8 +1010,8 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { } bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition) { - if (!hrtfEnabled || !renderer || !renderer->IsInitialized()) { - // If HRTF is disabled or renderer is not available, just copy input to output + if (!hrtfEnabled) { + // If HRTF is disabled, just copy input to output for (uint32_t i = 0; i < sampleCount; i++) { outputBuffer[i * 2] = inputBuffer[i]; // Left channel outputBuffer[i * 2 + 1] = inputBuffer[i]; // Right channel @@ -1010,6 +1019,54 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin return true; } + // Check if we should use CPU-only processing or if Vulkan is not available + if (hrtfCPUOnly || !renderer || !renderer->IsInitialized()) { + // Use CPU-based HRTF processing (either forced or fallback) + // Skip Vulkan buffer creation and go directly to CPU processing + } else { + // Use Vulkan shader-based HRTF processing + // Create buffers for HRTF processing if they don't exist or if the sample count has changed + if (!createHRTFBuffers(sampleCount)) { + std::cerr << "Failed to create HRTF buffers" << std::endl; + return false; + } + + // Copy input data to input buffer + void* data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); + memcpy(data, inputBuffer, sampleCount * sizeof(float)); + inputBufferMemory.unmapMemory(); + + // Set up HRTF parameters + struct HRTFParams { + float sourcePosition[3]; + float listenerPosition[3]; + float listenerOrientation[6]; // Forward (3) and up (3) vectors + uint32_t sampleCount; + uint32_t hrtfSize; + uint32_t numHrtfPositions; + float padding; // For alignment + } params; + + // Copy source and listener positions + memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); + memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); + memcpy(params.listenerOrientation, listenerOrientation, sizeof(float) * 6); + params.sampleCount = sampleCount; + params.hrtfSize = hrtfSize; + params.numHrtfPositions = numHrtfPositions; + params.padding = 0.0f; + + // Copy parameters to parameter buffer + data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); + memcpy(data, ¶ms, sizeof(HRTFParams)); + paramsBufferMemory.unmapMemory(); + + // TODO: Add actual Vulkan compute shader dispatch here + // For now, fall through to CPU processing + } + + // CPU-based HRTF processing (used for both CPU-only mode and fallback) + // Create buffers for HRTF processing if they don't exist or if the sample count has changed if (!createHRTFBuffers(sampleCount)) { std::cerr << "Failed to create HRTF buffers" << std::endl; diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index 22eea43f..0467e3da 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -232,6 +232,18 @@ class AudioSystem { */ bool IsHRTFEnabled() const; + /** + * @brief Set whether to force CPU-only HRTF processing. + * @param cpuOnly Whether to force CPU-only processing (true) or allow Vulkan shader processing (false). + */ + void SetHRTFCPUOnly(bool cpuOnly); + + /** + * @brief Check if HRTF processing is set to CPU-only mode. + * @return True if CPU-only mode is enabled, false if Vulkan shader processing is allowed. + */ + bool IsHRTFCPUOnly() const; + /** * @brief Load HRTF data from a file. * @param filename The path to the HRTF data file. @@ -277,6 +289,7 @@ class AudioSystem { // HRTF processing bool hrtfEnabled = false; + bool hrtfCPUOnly = false; std::vector hrtfData; uint32_t hrtfSize = 0; uint32_t numHrtfPositions = 0; diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 16ec66cc..cd7ca07f 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -210,6 +210,35 @@ void ImGuiSystem::NewFrame() { ImGui::Text("HRTF Processing: ENABLED"); ImGui::Text("Use directional buttons to move the audio source in 3D space"); ImGui::Text("You should hear the audio move around you!"); + + // HRTF Processing Mode Selection + ImGui::Separator(); + ImGui::Text("HRTF Processing Mode:"); + + static bool cpuOnlyMode = false; + static bool initialized = false; + + // Initialize checkbox state to match audio system state + if (!initialized && audioSystem) { + cpuOnlyMode = audioSystem->IsHRTFCPUOnly(); + initialized = true; + } + + if (ImGui::Checkbox("Force CPU-only HRTF processing", &cpuOnlyMode)) { + if (audioSystem) { + audioSystem->SetHRTFCPUOnly(cpuOnlyMode); + std::cout << "HRTF processing mode changed to: " << (cpuOnlyMode ? "CPU-only" : "Vulkan shader (when available)") << std::endl; + } + } + + // Display current processing mode + if (audioSystem) { + if (audioSystem->IsHRTFCPUOnly()) { + ImGui::Text("Current Mode: CPU-only processing"); + } else { + ImGui::Text("Current Mode: Vulkan shader processing (when available)"); + } + } } else { ImGui::Text("HRTF Processing: DISABLED"); } From 95135979b7e557e037691c4dc75d2fc14c006e71 Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 24 Jul 2025 18:21:30 -0700 Subject: [PATCH 028/102] audio fully works, GPU HRTF works as well as CPU HRTF. --- attachments/simple_engine/audio_system.cpp | 635 +++++++++++------- attachments/simple_engine/audio_system.h | 73 +- attachments/simple_engine/renderer.h | 44 +- .../simple_engine/renderer_compute.cpp | 75 ++- attachments/simple_engine/renderer_core.cpp | 31 +- .../simple_engine/renderer_rendering.cpp | 8 +- .../simple_engine/renderer_resources.cpp | 24 +- attachments/simple_engine/shaders/hrtf.slang | 246 +++++-- 8 files changed, 807 insertions(+), 329 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 74914d9f..ca73eb56 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -1,13 +1,16 @@ #include "audio_system.h" +#include #include #include #include +#include #include #include #include #include #include +#include // OpenAL headers #ifdef __APPLE__ @@ -52,7 +55,7 @@ static void CheckOpenALError(const std::string& operation) { // Concrete implementation of AudioSource class ConcreteAudioSource : public AudioSource { public: - explicit ConcreteAudioSource(const std::string& name) : name(name) {} + explicit ConcreteAudioSource(std::string name) : name(std::move(name)) {} ~ConcreteAudioSource() override = default; void Play() override { @@ -60,12 +63,10 @@ class ConcreteAudioSource : public AudioSource { playbackPosition = 0; delayTimer = 0.0f; inDelayPhase = false; - std::cout << "Playing audio source: " << name << std::endl; } void Pause() override { playing = false; - std::cout << "Pausing audio source: " << name << std::endl; } void Stop() override { @@ -73,31 +74,26 @@ class ConcreteAudioSource : public AudioSource { playbackPosition = 0; delayTimer = 0.0f; inDelayPhase = false; - std::cout << "Stopping audio source: " << name << std::endl; } void SetVolume(float volume) override { this->volume = volume; - std::cout << "Setting volume of audio source " << name << " to " << volume << std::endl; } void SetLoop(bool loop) override { this->loop = loop; - std::cout << "Setting loop of audio source " << name << " to " << (loop ? "true" : "false") << std::endl; } void SetPosition(float x, float y, float z) override { position[0] = x; position[1] = y; position[2] = z; - std::cout << "Setting position of audio source " << name << " to (" << x << ", " << y << ", " << z << ")" << std::endl; } void SetVelocity(float x, float y, float z) override { velocity[0] = x; velocity[1] = y; velocity[2] = z; - std::cout << "Setting velocity of audio source " << name << " to (" << x << ", " << y << ", " << z << ")" << std::endl; } [[nodiscard]] bool IsPlaying() const override { @@ -120,7 +116,6 @@ class ConcreteAudioSource : public AudioSource { inDelayPhase = false; playbackPosition = 0; delayTimer = 0.0f; - std::cout << "Delay finished, restarting audio playback for: " << name << std::endl; } } else { // Normal playback, update position @@ -129,15 +124,13 @@ class ConcreteAudioSource : public AudioSource { // Check if we've reached the end of the audio if (audioLengthSamples > 0 && playbackPosition >= audioLengthSamples) { if (loop) { - // Start delay phase before looping + // Start the delay phase before looping inDelayPhase = true; delayTimer = 0.0f; - std::cout << "Audio finished, starting 1.5s delay before loop for: " << name << std::endl; } else { // Stop playing if not looping playing = false; playbackPosition = 0; - std::cout << "Audio finished, stopping playback for: " << name << std::endl; } } } @@ -171,8 +164,8 @@ class ConcreteAudioSource : public AudioSource { uint32_t playbackPosition = 0; // Current position in samples uint32_t audioLengthSamples = 0; // Total length of audio in samples float delayTimer = 0.0f; // Timer for delay between loops - bool inDelayPhase = false; // Whether we're currently in delay phase - static constexpr float delayDuration = 1.5f; // 1.5 second delay between loops + bool inDelayPhase = false; // Whether we're currently in the delay phase + static constexpr float delayDuration = 1.5f; // 1.5-second delay between loops }; // OpenAL audio output device implementation @@ -180,7 +173,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { public: OpenALAudioOutputDevice() = default; ~OpenALAudioOutputDevice() override { - Stop(); + OpenALAudioOutputDevice::Stop(); Cleanup(); } @@ -242,7 +235,6 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { } initialized = true; - std::cout << "OpenAL audio output device initialized successfully" << std::endl; return true; } @@ -258,10 +250,9 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { playing = true; - // Start audio playback thread + // Start an audio playback thread audioThread = std::thread(&OpenALAudioOutputDevice::AudioThreadFunction, this); - std::cout << "OpenAL audio output device started" << std::endl; return true; } @@ -272,7 +263,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { playing = false; - // Wait for audio thread to finish + // Wait for the audio thread to finish if (audioThread.joinable()) { audioThread.join(); } @@ -283,7 +274,6 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { CheckOpenALError("alSourceStop"); } - std::cout << "OpenAL audio output device stopped" << std::endl; return true; } @@ -302,16 +292,16 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { return true; } - bool IsPlaying() const override { + [[nodiscard]] bool IsPlaying() const override { return playing; } - uint32_t GetPosition() const override { + [[nodiscard]] uint32_t GetPosition() const override { return playbackPosition; } private: - static const int NUM_BUFFERS = 4; + static constexpr int NUM_BUFFERS = 4; uint32_t sampleRate = 44100; uint32_t channels = 2; @@ -324,7 +314,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { ALCdevice* device = nullptr; ALCcontext* context = nullptr; ALuint source = 0; - ALuint buffers[NUM_BUFFERS]; + ALuint buffers[NUM_BUFFERS]{}; int currentBuffer = 0; std::vector audioBuffer; @@ -435,7 +425,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { } // Validate buffer parameters - if (samplesProcessed == 0 || pcmBuffer.empty()) { + if (pcmBuffer.empty()) { // Re-add buffer to available list if we can't use it if (queuedBufferCount >= NUM_BUFFERS) { availableBuffers.push(buffer); @@ -471,18 +461,14 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { } playbackPosition += samplesProcessed / channels; - - // For debugging: print audio activity - static uint32_t debugCounter = 0; - if (++debugCounter % 100 == 0) { // Print every 100 buffer updates - std::cout << "OpenAL output: processed " << samplesProcessed - << " samples, position: " << playbackPosition << std::endl; - } } } }; AudioSystem::~AudioSystem() { + // Stop the audio thread first + stopAudioThread(); + // Stop and clean up audio output device if (outputDevice) { outputDevice->Stop(); @@ -498,13 +484,17 @@ AudioSystem::~AudioSystem() { } void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition) { - const float sampleRate = 44100.0f; + constexpr float sampleRate = 44100.0f; const float frequency = 1000.0f; // 1000Hz ping - louder and more penetrating frequency - const float pingDuration = 0.5f; // 0.5 second ping duration - const uint32_t pingSamples = static_cast(pingDuration * sampleRate); - const float silenceDuration = 1.0f; // 1 second silence after ping - const uint32_t silenceSamples = static_cast(silenceDuration * sampleRate); - const uint32_t totalCycleSamples = pingSamples + silenceSamples; + constexpr float pingDuration = 0.5f; // 0.5 second ping duration + constexpr auto pingSamples = static_cast(pingDuration * sampleRate); + constexpr float silenceDuration = 1.0f; // 1 second silence after ping + constexpr auto silenceSamples = static_cast(silenceDuration * sampleRate); + constexpr uint32_t totalCycleSamples = pingSamples + silenceSamples; + + // Debug: Track generated values + float maxGenerated = 0.0f; + uint32_t nonZeroGenerated = 0; for (uint32_t i = 0; i < sampleCount; i++) { uint32_t globalPosition = playbackPosition + i; @@ -530,19 +520,25 @@ void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint } // Generate sine wave with envelope - float sineWave = std::sin(2.0f * M_PI * frequency * t); + float sineWave = sinf(2.0f * static_cast(M_PI) * frequency * t); buffer[i] = 0.8f * envelope * sineWave; // 0.8 amplitude for much louder, clearly audible sound + + // Track generated values for debugging + float absValue = std::abs(buffer[i]); + if (absValue > 0.0001f) { + nonZeroGenerated++; + } + maxGenerated = std::max(maxGenerated, absValue); } else { // Silence phase buffer[i] = 0.0f; } } + } bool AudioSystem::Initialize(Renderer* renderer) { if (renderer) { - std::cout << "Initializing HRTF audio system with Vulkan compute shader support" << std::endl; - // Validate renderer if provided if (!renderer->IsInitialized()) { std::cerr << "AudioSystem::Initialize: Renderer is not initialized" << std::endl; @@ -552,13 +548,11 @@ bool AudioSystem::Initialize(Renderer* renderer) { // Store the renderer for compute shader support this->renderer = renderer; } else { - std::cout << "Initializing HRTF audio system with CPU-based processing (no renderer)" << std::endl; this->renderer = nullptr; } // Generate default HRTF data for spatial audio processing LoadHRTFData(""); // Pass empty filename to force generation of default HRTF data - std::cout << "Using generated HRTF data for spatial audio processing" << std::endl; // Enable HRTF processing by default for 3D spatial audio EnableHRTF(true); @@ -582,8 +576,10 @@ bool AudioSystem::Initialize(Renderer* renderer) { return false; } + // Start the background audio processing thread + startAudioThread(); + initialized = true; - std::cout << "HRTF audio system initialized successfully with audio output" << std::endl; return true; } @@ -592,9 +588,6 @@ void AudioSystem::Update(float deltaTime) { return; } - // Check if we have a valid renderer for Vulkan compute shader processing - bool hasValidRenderer = (renderer && renderer->IsInitialized()); - // Update audio sources and process spatial audio for (auto& source : sources) { if (!source->IsPlaying()) { @@ -602,12 +595,12 @@ void AudioSystem::Update(float deltaTime) { } // Cast to ConcreteAudioSource to access timing methods - ConcreteAudioSource* concreteSource = static_cast(source.get()); + auto* concreteSource = dynamic_cast(source.get()); // Update playback timing and delay logic concreteSource->UpdatePlayback(deltaTime, 0); // Will update with actual samples processed later - // Only process audio if not in delay phase + // Only process audio if not in the delay phase if (!concreteSource->ShouldProcessAudio()) { continue; } @@ -618,7 +611,7 @@ void AudioSystem::Update(float deltaTime) { const float* sourcePosition = concreteSource->GetPosition(); // Create sample buffers for processing - const uint32_t sampleCount = 1024; + constexpr uint32_t sampleCount = 1024; std::vector inputBuffer(sampleCount, 0.0f); std::vector outputBuffer(sampleCount * 2, 0.0f); uint32_t actualSamplesProcessed = 0; @@ -649,21 +642,10 @@ void AudioSystem::Update(float deltaTime) { actualSamplesProcessed = sampleCount; } - // Process audio with HRTF spatial processing using Vulkan compute shader - ProcessHRTF(inputBuffer.data(), outputBuffer.data(), sampleCount, sourcePosition); - - // Send processed audio to output device - if (outputDevice && outputDevice->IsPlaying()) { - // Apply master volume - for (uint32_t i = 0; i < sampleCount * 2; i++) { - outputBuffer[i] *= masterVolume; - } - - // Send to audio output device - if (!outputDevice->WriteAudio(outputBuffer.data(), sampleCount)) { - std::cerr << "Failed to write audio data to output device" << std::endl; - } - } + // Process audio with HRTF spatial processing using background thread + // This prevents the main thread from blocking on fence waits + // The background thread handles the complete pipeline including output to device + submitAudioTask(inputBuffer.data(), sampleCount, sourcePosition, actualSamplesProcessed); // Update playback timing with actual samples processed concreteSource->UpdatePlayback(0.0f, actualSamplesProcessed); @@ -679,22 +661,19 @@ void AudioSystem::Update(float deltaTime) { } // Clean up finished audio sources - sources.erase( - std::remove_if(sources.begin(), sources.end(), - [](const std::unique_ptr& source) { - // Keep all sources active for continuous playback - // Audio sources can be stopped/started via their Play/Stop methods - return false; - }), - sources.end() - ); + std::erase_if(sources, + [](const std::unique_ptr& source) { + // Keep all sources active for continuous playback + // Audio sources can be stopped/started via their Play/Stop methods + return false; + }); // Update timing for audio processing with low-latency chunks static float accumulatedTime = 0.0f; accumulatedTime += deltaTime; // Process audio in 20ms chunks for optimal latency - const float audioChunkTime = 0.02f; // 20ms chunks for real-time audio + constexpr float audioChunkTime = 0.02f; // 20ms chunks for real-time audio if (accumulatedTime >= audioChunkTime) { // Trigger audio buffer updates for smooth playback // The HRTF processing ensures spatial audio is updated continuously @@ -706,7 +685,6 @@ void AudioSystem::Update(float deltaTime) { } bool AudioSystem::LoadAudio(const std::string& filename, const std::string& name) { - std::cout << "Loading audio file: " << filename << " as " << name << std::endl; // Open the WAV file std::ifstream file(filename, std::ios::binary); @@ -732,7 +710,7 @@ bool AudioSystem::LoadAudio(const std::string& filename, const std::string& name uint32_t dataSize; // Data size }; - WAVHeader header; + WAVHeader header{}; file.read(reinterpret_cast(&header), sizeof(WAVHeader)); // Validate WAV header @@ -765,12 +743,6 @@ bool AudioSystem::LoadAudio(const std::string& filename, const std::string& name // Store the audio data audioData[name] = std::move(data); - std::cout << "Successfully loaded WAV file: " << filename - << " (Channels: " << header.numChannels - << ", Sample Rate: " << header.sampleRate - << ", Bits: " << header.bitsPerSample - << ", Size: " << header.dataSize << " bytes)" << std::endl; - return true; } @@ -796,29 +768,25 @@ AudioSource* AudioSystem::CreateAudioSource(const std::string& name) { // Set the audio length for proper timing source->SetAudioLength(totalSamples); - std::cout << "Set audio length for " << name << ": " << totalSamples << " samples (corrected for 4-byte indexing)" << std::endl; } // Store the source AudioSource* sourcePtr = source.get(); sources.push_back(std::move(source)); - std::cout << "Audio source created: " << name << std::endl; return sourcePtr; } AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { - std::cout << "Creating debug ping audio source: " << name << std::endl; - // Create a new audio source for debugging auto source = std::make_unique(name); // Set up debug ping parameters // The ping will cycle every 1.5 seconds (0.5s ping + 1.0s silence) - const float sampleRate = 44100.0f; - const float pingDuration = 0.5f; - const float silenceDuration = 1.0f; - const uint32_t totalCycleSamples = static_cast((pingDuration + silenceDuration) * sampleRate); + constexpr float sampleRate = 44100.0f; + constexpr float pingDuration = 0.5f; + constexpr float silenceDuration = 1.0f; + constexpr auto totalCycleSamples = static_cast((pingDuration + silenceDuration) * sampleRate); // Set the audio length for proper timing (infinite loop for debugging) source->SetAudioLength(totalCycleSamples); @@ -827,7 +795,6 @@ AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { AudioSource* sourcePtr = source.get(); sources.push_back(std::move(source)); - std::cout << "Debug ping audio source created: " << name << " (800Hz ping every 1.5 seconds)" << std::endl; return sourcePtr; } @@ -835,8 +802,6 @@ void AudioSystem::SetListenerPosition(float x, float y, float z) { listenerPosition[0] = x; listenerPosition[1] = y; listenerPosition[2] = z; - - std::cout << "Setting listener position to (" << x << ", " << y << ", " << z << ")" << std::endl; } void AudioSystem::SetListenerOrientation(float forwardX, float forwardY, float forwardZ, @@ -847,28 +812,20 @@ void AudioSystem::SetListenerOrientation(float forwardX, float forwardY, float f listenerOrientation[3] = upX; listenerOrientation[4] = upY; listenerOrientation[5] = upZ; - - std::cout << "Setting listener orientation to forward=(" << forwardX << ", " << forwardY << ", " << forwardZ << "), " - << "up=(" << upX << ", " << upY << ", " << upZ << ")" << std::endl; } void AudioSystem::SetListenerVelocity(float x, float y, float z) { listenerVelocity[0] = x; listenerVelocity[1] = y; listenerVelocity[2] = z; - - std::cout << "Setting listener velocity to (" << x << ", " << y << ", " << z << ")" << std::endl; } void AudioSystem::SetMasterVolume(float volume) { masterVolume = volume; - - std::cout << "Setting master volume to " << volume << std::endl; } void AudioSystem::EnableHRTF(bool enable) { hrtfEnabled = enable; - std::cout << "HRTF processing " << (enable ? "enabled" : "disabled") << std::endl; } bool AudioSystem::IsHRTFEnabled() const { @@ -877,7 +834,6 @@ bool AudioSystem::IsHRTFEnabled() const { void AudioSystem::SetHRTFCPUOnly(bool cpuOnly) { hrtfCPUOnly = cpuOnly; - std::cout << "HRTF processing mode set to " << (cpuOnly ? "CPU-only" : "Vulkan shader (when available)") << std::endl; } bool AudioSystem::IsHRTFCPUOnly() const { @@ -885,16 +841,11 @@ bool AudioSystem::IsHRTFCPUOnly() const { } bool AudioSystem::LoadHRTFData(const std::string& filename) { - if (!filename.empty()) { - std::cout << "Loading HRTF data from: " << filename << std::endl; - } else { - std::cout << "Generating default HRTF data (no file specified)" << std::endl; - } // HRTF parameters - const uint32_t hrtfSampleCount = 256; // Number of samples per impulse response - const uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) - const uint32_t channelCount = 2; // Stereo (left and right ears) + constexpr uint32_t hrtfSampleCount = 256; // Number of samples per impulse response + constexpr uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) + constexpr uint32_t channelCount = 2; // Stereo (left and right ears) const float sampleRate = 44100.0f; // Sample rate for HRTF data const float speedOfSound = 343.0f; // Speed of sound in m/s const float headRadius = 0.0875f; // Average head radius in meters @@ -922,7 +873,6 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { numHrtfPositions = filePositionCount; file.close(); - std::cout << "Successfully loaded HRTF data from file" << std::endl; return true; } } @@ -931,8 +881,6 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { } // Generate realistic HRTF data based on acoustic modeling - std::cout << "Generating realistic HRTF impulse responses..." << std::endl; - // Resize the HRTF data vector hrtfData.resize(hrtfSampleCount * positionCount * channelCount); @@ -942,8 +890,8 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { uint32_t azimuthIndex = pos % 36; uint32_t elevationIndex = pos / 36; - float azimuth = (static_cast(azimuthIndex) * 10.0f - 180.0f) * M_PI / 180.0f; - float elevation = (static_cast(elevationIndex) * 15.0f - 90.0f) * M_PI / 180.0f; + float azimuth = (static_cast(azimuthIndex) * 10.0f - 180.0f) * static_cast(M_PI) / 180.0f; + float elevation = (static_cast(elevationIndex) * 15.0f - 90.0f) * static_cast(M_PI) / 180.0f; // Convert to Cartesian coordinates float x = std::cos(elevation) * std::sin(azimuth); @@ -962,7 +910,7 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { // Calculate time delay (ITD - Interaural Time Difference) float timeDelay = distance / speedOfSound; - uint32_t sampleDelay = static_cast(timeDelay * sampleRate); + auto sampleDelay = static_cast(timeDelay * sampleRate); // Calculate head shadow effect (ILD - Interaural Level Difference) float shadowFactor = 1.0f; @@ -972,14 +920,16 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { shadowFactor = 0.3f + 0.7f * std::exp(azimuth * 2.0f); } + // Generate impulse response + uint32_t samplesGenerated = 0; for (uint32_t i = 0; i < hrtfSampleCount; i++) { float value = 0.0f; // Direct path impulse if (i >= sampleDelay && i < sampleDelay + 10) { float t = static_cast(i - sampleDelay) / sampleRate; - value = shadowFactor * std::exp(-t * 1000.0f) * std::cos(2.0f * M_PI * 1000.0f * t); + value = shadowFactor * std::exp(-t * 1000.0f) * std::cos(2.0f * static_cast(M_PI) * 1000.0f * t); } // Early reflections (simplified) @@ -988,7 +938,7 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { if (i >= reflDelay && i < reflDelay + 5) { float t = static_cast(i - reflDelay) / sampleRate; float reflGain = shadowFactor * 0.3f / static_cast(refl); - value += reflGain * std::exp(-t * 2000.0f) * std::cos(2.0f * M_PI * 800.0f * t); + value += reflGain * std::exp(-t * 2000.0f) * std::cos(2.0f * static_cast(M_PI) * 800.0f * t); } } @@ -1005,11 +955,11 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { hrtfSize = hrtfSampleCount; numHrtfPositions = positionCount; - std::cout << "Successfully generated " << positionCount << " HRTF impulse responses" << std::endl; return true; } bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition) { + if (!hrtfEnabled) { // If HRTF is disabled, just copy input to output for (uint32_t i = 0; i < sampleCount; i++) { @@ -1020,11 +970,11 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin } // Check if we should use CPU-only processing or if Vulkan is not available - if (hrtfCPUOnly || !renderer || !renderer->IsInitialized()) { + // Also force CPU processing if we've detected threading issues previously + static bool forceGPUFallback = false; + if (hrtfCPUOnly || !renderer || !renderer->IsInitialized() || forceGPUFallback) { // Use CPU-based HRTF processing (either forced or fallback) - // Skip Vulkan buffer creation and go directly to CPU processing - } else { - // Use Vulkan shader-based HRTF processing + // Create buffers for HRTF processing if they don't exist or if the sample count has changed if (!createHRTFBuffers(sampleCount)) { std::cerr << "Failed to create HRTF buffers" << std::endl; @@ -1036,17 +986,6 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin memcpy(data, inputBuffer, sampleCount * sizeof(float)); inputBufferMemory.unmapMemory(); - // Set up HRTF parameters - struct HRTFParams { - float sourcePosition[3]; - float listenerPosition[3]; - float listenerOrientation[6]; // Forward (3) and up (3) vectors - uint32_t sampleCount; - uint32_t hrtfSize; - uint32_t numHrtfPositions; - float padding; // For alignment - } params; - // Copy source and listener positions memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); @@ -1056,118 +995,198 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin params.numHrtfPositions = numHrtfPositions; params.padding = 0.0f; - // Copy parameters to parameter buffer - data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); - memcpy(data, ¶ms, sizeof(HRTFParams)); - paramsBufferMemory.unmapMemory(); + // Copy parameters to parameter buffer using persistent memory mapping + if (persistentParamsMemory) { + memcpy(persistentParamsMemory, ¶ms, sizeof(HRTFParams)); + } else { + std::cerr << "WARNING: Persistent memory not available, falling back to map/unmap" << std::endl; + data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); + memcpy(data, ¶ms, sizeof(HRTFParams)); + paramsBufferMemory.unmapMemory(); + } - // TODO: Add actual Vulkan compute shader dispatch here - // For now, fall through to CPU processing - } + // Perform HRTF processing using CPU-based convolution + // This implementation provides real-time 3D audio spatialization + + // Calculate direction from listener to source + float direction[3]; + direction[0] = sourcePosition[0] - listenerPosition[0]; + direction[1] = sourcePosition[1] - listenerPosition[1]; + direction[2] = sourcePosition[2] - listenerPosition[2]; + + // Normalize direction + float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); + if (length > 0.0001f) { + direction[0] /= length; + direction[1] /= length; + direction[2] /= length; + } else { + direction[0] = 0.0f; + direction[1] = 0.0f; + direction[2] = -1.0f; // Default to front + } - // CPU-based HRTF processing (used for both CPU-only mode and fallback) + // Calculate azimuth and elevation + float azimuth = std::atan2(direction[0], direction[2]); + float elevation = std::asin(std::max(-1.0f, std::min(1.0f, direction[1]))); - // Create buffers for HRTF processing if they don't exist or if the sample count has changed - if (!createHRTFBuffers(sampleCount)) { - std::cerr << "Failed to create HRTF buffers" << std::endl; - return false; - } + // Convert to indices + int azimuthIndex = static_cast((azimuth + M_PI) / (2.0f * M_PI) * 36.0f) % 36; + int elevationIndex = static_cast((elevation + M_PI / 2.0f) / M_PI * 13.0f); + elevationIndex = std::max(0, std::min(12, elevationIndex)); + + // Get HRTF index + int hrtfIndex = elevationIndex * 36 + azimuthIndex; + hrtfIndex = std::min(hrtfIndex, static_cast(numHrtfPositions) - 1); + + // Perform convolution for left and right ears + for (uint32_t i = 0; i < sampleCount; i++) { + float leftSample = 0.0f; + float rightSample = 0.0f; + + // Convolve with HRTF impulse response + for (uint32_t j = 0; j < hrtfSize && j <= i; j++) { + uint32_t hrtfLeftIndex = hrtfIndex * hrtfSize * 2 + j; + uint32_t hrtfRightIndex = hrtfIndex * hrtfSize * 2 + hrtfSize + j; + + if (hrtfLeftIndex < hrtfData.size() && hrtfRightIndex < hrtfData.size()) { + leftSample += inputBuffer[i - j] * hrtfData[hrtfLeftIndex]; + rightSample += inputBuffer[i - j] * hrtfData[hrtfRightIndex]; + } + } + + // Apply distance attenuation + float distanceAttenuation = 1.0f / std::max(1.0f, length); + leftSample *= distanceAttenuation; + rightSample *= distanceAttenuation; - // Copy input data to input buffer - void* data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); - memcpy(data, inputBuffer, sampleCount * sizeof(float)); - inputBufferMemory.unmapMemory(); - - // Set up HRTF parameters - struct HRTFParams { - float sourcePosition[3]; - float listenerPosition[3]; - float listenerOrientation[6]; // Forward (3) and up (3) vectors - uint32_t sampleCount; - uint32_t hrtfSize; - uint32_t numHrtfPositions; - float padding; // For alignment - } params; - - // Copy source and listener positions - memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); - memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); - memcpy(params.listenerOrientation, listenerOrientation, sizeof(float) * 6); - params.sampleCount = sampleCount; - params.hrtfSize = hrtfSize; - params.numHrtfPositions = numHrtfPositions; - params.padding = 0.0f; - - // Copy parameters to parameter buffer - data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); - memcpy(data, ¶ms, sizeof(HRTFParams)); - paramsBufferMemory.unmapMemory(); - - // Perform HRTF processing using CPU-based convolution - // This implementation provides real-time 3D audio spatialization - - // Calculate direction from listener to source - float direction[3]; - direction[0] = sourcePosition[0] - listenerPosition[0]; - direction[1] = sourcePosition[1] - listenerPosition[1]; - direction[2] = sourcePosition[2] - listenerPosition[2]; - - // Normalize direction - float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); - if (length > 0.0001f) { - direction[0] /= length; - direction[1] /= length; - direction[2] /= length; + // Write to output buffer + outputBuffer[i * 2] = leftSample; + outputBuffer[i * 2 + 1] = rightSample; + } + + + + return true; } else { - direction[0] = 0.0f; - direction[1] = 0.0f; - direction[2] = -1.0f; // Default to front - } + // Use Vulkan shader-based HRTF processing with fallback to CPU + try { + // Validate HRTF data exists + if (hrtfData.empty()) { + LoadHRTFData(""); // Generate HRTF data + } - // Calculate azimuth and elevation - float azimuth = std::atan2(direction[0], direction[2]); - float elevation = std::asin(std::max(-1.0f, std::min(1.0f, direction[1]))); + // Create buffers for HRTF processing if they don't exist or if the sample count has changed + if (!createHRTFBuffers(sampleCount)) { + std::cerr << "Failed to create HRTF buffers, falling back to CPU processing" << std::endl; + throw std::runtime_error("Buffer creation failed"); + } - // Convert to indices - int azimuthIndex = static_cast((azimuth + M_PI) / (2.0f * M_PI) * 36.0f) % 36; - int elevationIndex = static_cast((elevation + M_PI / 2.0f) / M_PI * 13.0f); - elevationIndex = std::max(0, std::min(12, elevationIndex)); + // Copy input data to input buffer + void* data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); + memcpy(data, inputBuffer, sampleCount * sizeof(float)); + + + inputBufferMemory.unmapMemory(); + + // Set up HRTF parameters with proper std140 uniform buffer layout + struct alignas(16) HRTFParams { + float listenerPosition[4]; // vec3 + padding (16 bytes) - offset 0 + float listenerForward[4]; // vec3 + padding (16 bytes) - offset 16 + float listenerUp[4]; // vec3 + padding (16 bytes) - offset 32 + float sourcePosition[4]; // vec3 + padding (16 bytes) - offset 48 + float sampleCount; // float (4 bytes) - offset 64 + float padding1[3]; // Padding to align to 16-byte boundary - offset 68 + uint32_t inputChannels; // uint (4 bytes) - offset 80 + uint32_t outputChannels; // uint (4 bytes) - offset 84 + uint32_t hrtfSize; // uint (4 bytes) - offset 88 + uint32_t numHrtfPositions; // uint (4 bytes) - offset 92 + float distanceAttenuation; // float (4 bytes) - offset 96 + float dopplerFactor; // float (4 bytes) - offset 100 + float reverbMix; // float (4 bytes) - offset 104 + float padding2; // Padding to complete 16-byte alignment - offset 108 + } params{}; + + // Copy listener and source positions with proper padding for GPU alignment + memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); + params.listenerPosition[3] = 0.0f; // Padding for float3 alignment + memcpy(params.listenerForward, &listenerOrientation[0], sizeof(float) * 3); // Forward vector + params.listenerForward[3] = 0.0f; // Padding for float3 alignment + memcpy(params.listenerUp, &listenerOrientation[3], sizeof(float) * 3); // Up vector + params.listenerUp[3] = 0.0f; // Padding for float3 alignment + memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); + params.sourcePosition[3] = 0.0f; // Padding for float3 alignment + params.sampleCount = static_cast(sampleCount); // Number of samples to process + params.padding1[0] = params.padding1[1] = params.padding1[2] = 0.0f; // Initialize padding + params.inputChannels = 1; // Mono input + params.outputChannels = 2; // Stereo output + params.hrtfSize = hrtfSize; + params.numHrtfPositions = numHrtfPositions; + params.distanceAttenuation = 1.0f; + params.dopplerFactor = 1.0f; + params.reverbMix = 0.0f; + params.padding2 = 0.0f; // Initialize padding + + // Copy parameters to parameter buffer using persistent memory mapping + if (persistentParamsMemory) { + memcpy(persistentParamsMemory, ¶ms, sizeof(HRTFParams)); + } else { + std::cerr << "ERROR: Persistent memory not available for GPU processing!" << std::endl; + throw std::runtime_error("Persistent memory required for GPU processing"); + } - // Get HRTF index - int hrtfIndex = elevationIndex * 36 + azimuthIndex; - hrtfIndex = std::min(hrtfIndex, static_cast(numHrtfPositions) - 1); - // Perform convolution for left and right ears - for (uint32_t i = 0; i < sampleCount; i++) { - float leftSample = 0.0f; - float rightSample = 0.0f; + // Use renderer's main compute pipeline instead of dedicated HRTF pipeline + uint32_t workGroupSize = 64; // Must match the numthreads in the shader + uint32_t groupCountX = (sampleCount + workGroupSize - 1) / workGroupSize; + - // Convolve with HRTF impulse response - for (uint32_t j = 0; j < hrtfSize && j <= i; j++) { - uint32_t hrtfLeftIndex = hrtfIndex * hrtfSize * 2 + j; - uint32_t hrtfRightIndex = hrtfIndex * hrtfSize * 2 + hrtfSize + j; - if (hrtfLeftIndex < hrtfData.size() && hrtfRightIndex < hrtfData.size()) { - leftSample += inputBuffer[i - j] * hrtfData[hrtfLeftIndex]; - rightSample += inputBuffer[i - j] * hrtfData[hrtfRightIndex]; + // Use renderer's main compute pipeline dispatch method + auto computeFence = renderer->DispatchCompute(groupCountX, 1, 1, + *this->inputBuffer, *this->outputBuffer, + *this->hrtfBuffer, *this->paramsBuffer); + + // Wait for compute shader to complete using fence-based synchronization + const vk::raii::Device& device = renderer->GetRaiiDevice(); + vk::Result result = device.waitForFences(*computeFence, VK_TRUE, UINT64_MAX); + if (result != vk::Result::eSuccess) { + std::cerr << "Failed to wait for compute fence: " << vk::to_string(result) << std::endl; + throw std::runtime_error("Fence wait failed"); } - } - // Apply distance attenuation - float distanceAttenuation = 1.0f / std::max(1.0f, length); - leftSample *= distanceAttenuation; - rightSample *= distanceAttenuation; - // Write to output buffer - outputBuffer[i * 2] = leftSample; - outputBuffer[i * 2 + 1] = rightSample; - } + // Copy results from output buffer to the output array + void* outputData = outputBufferMemory.mapMemory(0, sampleCount * 2 * sizeof(float)); - return true; + + memcpy(outputBuffer, outputData, sampleCount * 2 * sizeof(float)); + outputBufferMemory.unmapMemory(); + + + + return true; + } catch (const std::exception& e) { + std::cerr << "GPU HRTF processing failed: " << e.what() << std::endl; + std::cerr << "CPU fallback disabled - GPU path required" << std::endl; + throw; // Re-throw the exception to ensure failure without CPU fallback + } + } } bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { - // Clean up existing buffers + // Smart buffer reuse: only recreate if sample count changed significantly or buffers don't exist + if (currentSampleCount == sampleCount && *inputBuffer && *outputBuffer && *hrtfBuffer && *paramsBuffer) { + return true; + } + + // Ensure all GPU operations complete before cleaning up existing buffers + if (renderer) { + const vk::raii::Device& device = renderer->GetRaiiDevice(); + device.waitIdle(); + } + + // Clean up existing buffers only if we need to recreate them cleanupHRTFBuffers(); if (!renderer) { @@ -1242,9 +1261,27 @@ bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { memcpy(hrtfMappedMemory, hrtfData.data(), hrtfData.size() * sizeof(float)); hrtfBufferMemory.unmapMemory(); - // Create parameters buffer + // Create parameters buffer - use the correct GPU structure size + // The GPU processing uses a larger aligned structure (112 bytes) not the header struct (64 bytes) + struct alignas(16) GPUHRTFParams { + float listenerPosition[4]; // vec3 + padding (16 bytes) + float listenerForward[4]; // vec3 + padding (16 bytes) + float listenerUp[4]; // vec3 + padding (16 bytes) + float sourcePosition[4]; // vec3 + padding (16 bytes) + float sampleCount; // float (4 bytes) + float padding1[3]; // Padding to align to 16-byte boundary + uint32_t inputChannels; // uint (4 bytes) + uint32_t outputChannels; // uint (4 bytes) + uint32_t hrtfSize; // uint (4 bytes) + uint32_t numHrtfPositions; // uint (4 bytes) + float distanceAttenuation; // float (4 bytes) + float dopplerFactor; // float (4 bytes) + float reverbMix; // float (4 bytes) + float padding2; // Padding to complete 16-byte alignment + }; + vk::BufferCreateInfo paramsBufferInfo; - paramsBufferInfo.size = 256; // Size large enough for all parameters + paramsBufferInfo.size = sizeof(GPUHRTFParams); // Use correct GPU structure size (112 bytes) paramsBufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; paramsBufferInfo.sharingMode = vk::SharingMode::eExclusive; @@ -1262,6 +1299,10 @@ bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { paramsBufferMemory = vk::raii::DeviceMemory(device, paramsAllocInfo); paramsBuffer.bindMemory(*paramsBufferMemory, 0); + // Set up persistent memory mapping for parameters buffer to avoid repeated map/unmap operations + persistentParamsMemory = paramsBufferMemory.mapMemory(0, sizeof(GPUHRTFParams)); + // Update current sample count to track buffer size + currentSampleCount = sampleCount; return true; } catch (const std::exception& e) { @@ -1272,6 +1313,12 @@ bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { } void AudioSystem::cleanupHRTFBuffers() { + // Unmap persistent memory if it exists + if (persistentParamsMemory && *paramsBufferMemory) { + paramsBufferMemory.unmapMemory(); + persistentParamsMemory = nullptr; + } + // With RAII, we just need to set the resources to nullptr // The destructors will handle the cleanup inputBuffer = nullptr; @@ -1282,4 +1329,130 @@ void AudioSystem::cleanupHRTFBuffers() { hrtfBufferMemory = nullptr; paramsBuffer = nullptr; paramsBufferMemory = nullptr; + + // Reset sample count tracking + currentSampleCount = 0; +} + + +// Threading implementation methods + +void AudioSystem::startAudioThread() { + if (audioThreadRunning.load()) { + return; // Thread already running + } + + audioThreadShouldStop.store(false); + audioThreadRunning.store(true); + + audioThread = std::thread(&AudioSystem::audioThreadLoop, this); + std::cout << "Audio processing thread started" << std::endl; +} + +void AudioSystem::stopAudioThread() { + if (!audioThreadRunning.load()) { + return; // Thread not running + } + + // Signal the thread to stop + audioThreadShouldStop.store(true); + + // Wake up the thread if it's waiting + audioCondition.notify_all(); + + // Wait for the thread to finish + if (audioThread.joinable()) { + audioThread.join(); + } + + audioThreadRunning.store(false); + std::cout << "Audio processing thread stopped" << std::endl; +} + +void AudioSystem::audioThreadLoop() { + while (!audioThreadShouldStop.load()) { + std::shared_ptr task = nullptr; + + // Wait for a task or stop signal + { + std::unique_lock lock(taskQueueMutex); + audioCondition.wait(lock, [this] { + return !audioTaskQueue.empty() || audioThreadShouldStop.load(); + }); + + if (audioThreadShouldStop.load()) { + break; + } + + if (!audioTaskQueue.empty()) { + task = audioTaskQueue.front(); + audioTaskQueue.pop(); + } + } + + // Process the task if we have one + if (task) { + processAudioTask(task); + } + } +} + +void AudioSystem::processAudioTask(const std::shared_ptr& task) { + // Process HRTF in the background thread + bool success = ProcessHRTF(task->inputBuffer.data(), task->outputBuffer.data(), + task->sampleCount, task->sourcePosition); + + if (success && task->outputDevice && task->outputDevice->IsPlaying()) { + // Apply master volume in the background thread + for (uint32_t i = 0; i < task->sampleCount * 2; i++) { + task->outputBuffer[i] *= task->masterVolume; + } + + // Send processed audio directly to output device from background thread + if (!task->outputDevice->WriteAudio(task->outputBuffer.data(), task->sampleCount)) { + std::cerr << "Failed to write audio data to output device from background thread" << std::endl; + } + } +} + +bool AudioSystem::submitAudioTask(const float* inputBuffer, uint32_t sampleCount, + const float* sourcePosition, uint32_t actualSamplesProcessed) { + if (!audioThreadRunning.load()) { + // Fallback to synchronous processing if the thread is not running + std::vector outputBuffer(sampleCount * 2); + bool success = ProcessHRTF(inputBuffer, outputBuffer.data(), sampleCount, sourcePosition); + + if (success && outputDevice && outputDevice->IsPlaying()) { + // Apply master volume + for (uint32_t i = 0; i < sampleCount * 2; i++) { + outputBuffer[i] *= masterVolume; + } + + // Send to audio output device + if (!outputDevice->WriteAudio(outputBuffer.data(), sampleCount)) { + std::cerr << "Failed to write audio data to output device" << std::endl; + return false; + } + } + return success; + } + + // Create a new task for asynchronous processing + auto task = std::make_shared(); + task->inputBuffer.assign(inputBuffer, inputBuffer + sampleCount); + task->outputBuffer.resize(sampleCount * 2); // Stereo output + memcpy(task->sourcePosition, sourcePosition, sizeof(float) * 3); + task->sampleCount = sampleCount; + task->actualSamplesProcessed = actualSamplesProcessed; + task->outputDevice = outputDevice.get(); + task->masterVolume = masterVolume; + + // Submit the task to the queue (non-blocking) + { + std::lock_guard lock(taskQueueMutex); + audioTaskQueue.push(task); + } + audioCondition.notify_one(); + + return true; // Return immediately without waiting } diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index 0467e3da..0084d2d9 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -4,6 +4,11 @@ #include #include #include +#include +#include +#include +#include +#include #ifdef __INTELLISENSE__ #include #else @@ -267,7 +272,7 @@ class AudioSystem { * @param sampleCount The number of samples to generate. * @param playbackPosition The current playback position for timing. */ - void GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition); + static void GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition); private: // Loaded audio data @@ -300,6 +305,36 @@ class AudioSystem { // Audio output device for sending processed audio to speakers std::unique_ptr outputDevice = nullptr; + // Threading infrastructure for background audio processing + std::thread audioThread; + std::mutex audioMutex; + std::condition_variable audioCondition; + std::atomic audioThreadRunning{false}; + std::atomic audioThreadShouldStop{false}; + + // Audio processing task queue + struct AudioTask { + std::vector inputBuffer; + std::vector outputBuffer; + float sourcePosition[3]; + uint32_t sampleCount; + uint32_t actualSamplesProcessed; + AudioOutputDevice* outputDevice; + float masterVolume; + }; + // Set up HRTF parameters + struct HRTFParams { + float sourcePosition[3]; + float listenerPosition[3]; + float listenerOrientation[6]; // Forward (3) and up (3) vectors + uint32_t sampleCount; + uint32_t hrtfSize; + uint32_t numHrtfPositions; + float padding; // For alignment + } params; + std::queue> audioTaskQueue; + std::mutex taskQueueMutex; + // Vulkan resources for HRTF processing vk::raii::Buffer inputBuffer = nullptr; vk::raii::DeviceMemory inputBufferMemory = nullptr; @@ -310,6 +345,10 @@ class AudioSystem { vk::raii::Buffer paramsBuffer = nullptr; vk::raii::DeviceMemory paramsBufferMemory = nullptr; + // Persistent memory mapping for UBO to avoid repeated map/unmap operations + void* persistentParamsMemory = nullptr; + uint32_t currentSampleCount = 0; // Track current buffer size to avoid unnecessary recreation + /** * @brief Create buffers for HRTF processing. * @param sampleCount The number of samples to process. @@ -321,4 +360,36 @@ class AudioSystem { * @brief Clean up HRTF buffers. */ void cleanupHRTFBuffers(); + + + /** + * @brief Start the background audio processing thread. + */ + void startAudioThread(); + + /** + * @brief Stop the background audio processing thread. + */ + void stopAudioThread(); + + /** + * @brief Main loop for the background audio processing thread. + */ + void audioThreadLoop(); + + /** + * @brief Process an audio task in the background thread. + * @param task The audio task to process. + */ + void processAudioTask(const std::shared_ptr& task); + + /** + * @brief Submit an audio processing task to the background thread. + * @param inputBuffer The input audio buffer. + * @param sampleCount The number of samples to process. + * @param sourcePosition The position of the sound source. + * @param actualSamplesProcessed The number of samples actually processed. + * @return True if the task was submitted successfully, false otherwise. + */ + bool submitAudioTask(const float* inputBuffer, uint32_t sampleCount, const float* sourcePosition, uint32_t actualSamplesProcessed); }; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 36cb4101..251067fb 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -11,6 +11,7 @@ import vulkan_hpp; #include #include #include +#include #include "platform.h" #include "entity.h" @@ -119,10 +120,11 @@ class Renderer { * @param outputBuffer The output buffer. * @param hrtfBuffer The HRTF data buffer. * @param paramsBuffer The parameters buffer. + * @return A fence that can be used to synchronize with the compute operation. */ - void DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, - vk::Buffer inputBuffer, vk::Buffer outputBuffer, - vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer); + vk::raii::Fence DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, + vk::Buffer inputBuffer, vk::Buffer outputBuffer, + vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer); /** * @brief Check if the renderer is initialized. @@ -146,7 +148,10 @@ class Renderer { * @brief Get the compute queue. * @return The compute queue. */ - vk::Queue GetComputeQueue() const { return *computeQueue; } + vk::Queue GetComputeQueue() const { + std::lock_guard lock(queueMutex); + return *computeQueue; + } /** * @brief Find a suitable memory type. @@ -158,6 +163,32 @@ class Renderer { return findMemoryType(typeFilter, properties); } + /** + * @brief Get the compute queue family index. + * @return The compute queue family index. + */ + uint32_t GetComputeQueueFamilyIndex() const { + return queueFamilyIndices.computeFamily.value(); + } + + /** + * @brief Submit a command buffer to the compute queue with proper dispatch loader preservation. + * @param commandBuffer The command buffer to submit. + * @param fence The fence to signal when the operation completes. + */ + void SubmitToComputeQueue(vk::CommandBuffer commandBuffer, vk::Fence fence) { + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer + }; + + // Use mutex to ensure thread-safe access to compute queue + { + std::lock_guard lock(queueMutex); + computeQueue.submit(submitInfo, fence); + } + } + /** * @brief Create a shader module from SPIR-V code. * @param code The SPIR-V code. @@ -278,6 +309,10 @@ class Renderer { vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; vk::raii::DescriptorPool computeDescriptorPool = nullptr; std::vector computeDescriptorSets; + vk::raii::CommandPool computeCommandPool = nullptr; + + // Thread safety for queue access - unified mutex since queues may share the same underlying VkQueue + mutable std::mutex queueMutex; // Command pool and buffers vk::raii::CommandPool commandPool = nullptr; @@ -385,6 +420,7 @@ class Renderer { bool createComputePipeline(); void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material); bool createCommandPool(); + bool createComputeCommandPool(); bool createDepthResources(); bool createTextureImage(const std::string& texturePath, TextureResources& resources); bool createTextureImageView(TextureResources& resources); diff --git a/attachments/simple_engine/renderer_compute.cpp b/attachments/simple_engine/renderer_compute.cpp index ec01f250..f00c75e6 100644 --- a/attachments/simple_engine/renderer_compute.cpp +++ b/attachments/simple_engine/renderer_compute.cpp @@ -9,7 +9,7 @@ bool Renderer::createComputePipeline() { try { // Read compute shader code - auto computeShaderCode = readFile("shaders/compute.spv"); + auto computeShaderCode = readFile("shaders/hrtf.spv"); // Create shader module vk::raii::ShaderModule computeShaderModule = createShaderModule(computeShaderCode); @@ -99,18 +99,38 @@ bool Renderer::createComputePipeline() { computeDescriptorPool = vk::raii::DescriptorPool(device, poolInfo); - return true; + return createComputeCommandPool(); } catch (const std::exception& e) { std::cerr << "Failed to create compute pipeline: " << e.what() << std::endl; return false; } } +// Create compute command pool +bool Renderer::createComputeCommandPool() { + try { + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.computeFamily.value() + }; + + computeCommandPool = vk::raii::CommandPool(device, poolInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create compute command pool: " << e.what() << std::endl; + return false; + } +} + // Dispatch compute shader -void Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, - vk::Buffer inputBuffer, vk::Buffer outputBuffer, - vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer) { +vk::raii::Fence Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, + vk::Buffer inputBuffer, vk::Buffer outputBuffer, + vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer) { try { + // Create fence for synchronization + vk::FenceCreateInfo fenceInfo{}; + vk::raii::Fence computeFence(device, fenceInfo); + // Create descriptor sets vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *computeDescriptorPool, @@ -182,47 +202,62 @@ void Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint3 device.updateDescriptorSets(descriptorWrites, {}); - // Create command buffer + // Create command buffer using dedicated compute command pool vk::CommandBufferAllocateInfo cmdAllocInfo{ - .commandPool = *commandPool, + .commandPool = *computeCommandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; auto commandBuffers = device.allocateCommandBuffers(cmdAllocInfo); - vk::CommandBuffer cmdBuffer = commandBuffers[0]; - vk::raii::CommandBuffer commandBuffer(device, cmdBuffer, *commandPool); + // Use RAII wrapper temporarily for recording to preserve dispatch loader + vk::raii::CommandBuffer commandBufferRaii = std::move(commandBuffers[0]); // Begin command buffer vk::CommandBufferBeginInfo beginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; - commandBuffer.begin(beginInfo); + commandBufferRaii.begin(beginInfo); // Bind compute pipeline - commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *computePipeline); + commandBufferRaii.bindPipeline(vk::PipelineBindPoint::eCompute, *computePipeline); - // Bind descriptor sets - commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, reinterpret_cast &>(computeDescriptorSets), {}); + // Bind descriptor sets - properly convert RAII descriptor set to regular descriptor set + std::vector descriptorSetsToBindRaw; + descriptorSetsToBindRaw.reserve(1); + descriptorSetsToBindRaw.push_back(*computeDescriptorSets[0]); + commandBufferRaii.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, descriptorSetsToBindRaw, {}); // Dispatch compute shader - commandBuffer.dispatch(groupCountX, groupCountY, groupCountZ); + commandBufferRaii.dispatch(groupCountX, groupCountY, groupCountZ); // End command buffer - commandBuffer.end(); + commandBufferRaii.end(); + + // Extract raw command buffer for submission and release RAII ownership + // This prevents premature destruction while preserving the recorded commands + vk::CommandBuffer rawCommandBuffer = *commandBufferRaii; + commandBufferRaii.release(); // Release RAII ownership to prevent destruction - // Submit command buffer + // Submit command buffer with fence for synchronization vk::SubmitInfo submitInfo{ .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffer + .pCommandBuffers = &rawCommandBuffer }; - computeQueue.submit(submitInfo, nullptr); + // Use mutex to ensure thread-safe access to compute queue + { + std::lock_guard lock(queueMutex); + computeQueue.submit(submitInfo, *computeFence); + } - // Wait for compute to complete - computeQueue.waitIdle(); + // Return fence for non-blocking synchronization + return computeFence; } catch (const std::exception& e) { std::cerr << "Failed to dispatch compute shader: " << e.what() << std::endl; + // Return a null fence on error + vk::FenceCreateInfo fenceInfo{}; + return vk::raii::Fence(device, fenceInfo); } } diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 1d8769b5..f1e5acfc 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -21,10 +21,21 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { - if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { - // Print message to console - std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; - } + // // Check if this is a shader debug printf message + // if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) { + // std::string message(pCallbackData->pMessage); + // if (message.find("DEBUG-PRINTF") != std::string::npos) { + // // This is a shader debug printf message - always show it + // std::cout << "FINDME ===== SHADER DEBUG: " << pCallbackData->pMessage << std::endl; + // return VK_FALSE; + // } + // } + printf("Received %s\n", pCallbackData->pMessage); + + // if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { + // // Print message to console + // std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; + // } return VK_FALSE; } @@ -222,6 +233,9 @@ bool Renderer::createInstance(const std::string& appName, bool enableValidationL }; // Enable validation layers if requested + vk::ValidationFeaturesEXT validationFeatures{}; + std::vector enabledValidationFeatures; + if (enableValidationLayers) { if (!checkValidationLayerSupport()) { std::cerr << "Validation layers requested, but not available" << std::endl; @@ -230,6 +244,14 @@ bool Renderer::createInstance(const std::string& appName, bool enableValidationL createInfo.enabledLayerCount = static_cast(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); + + // Enable debug printf functionality for shader debugging + enabledValidationFeatures.push_back(vk::ValidationFeatureEnableEXT::eDebugPrintf); + + validationFeatures.enabledValidationFeatureCount = static_cast(enabledValidationFeatures.size()); + validationFeatures.pEnabledValidationFeatures = enabledValidationFeatures.data(); + + createInfo.pNext = &validationFeatures; } // Create instance @@ -251,6 +273,7 @@ bool Renderer::setupDebugMessenger(bool enableValidationLayers) { // Create debug messenger info vk::DebugUtilsMessengerCreateInfoEXT createInfo{ .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError, .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 6cdb5a7e..db0e705e 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -517,7 +517,11 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam .pSignalSemaphores = &*renderFinishedSemaphores[semaphoreIndex] }; - graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + // Use mutex to ensure thread-safe access to graphics queue + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + } // Present the image vk::PresentInfoKHR presentInfo{ @@ -528,7 +532,9 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam .pImageIndices = &imageIndex }; + // Use mutex to ensure thread-safe access to present queue try { + std::lock_guard lock(queueMutex); result.first = presentQueue.presentKHR(presentInfo); } catch (const vk::OutOfDateKHRError&) { framebufferResized = true; diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index c0ed384c..f5f7a8b2 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -594,8 +594,12 @@ void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuff .pCommandBuffers = &*commandBuffer }; - graphicsQueue.submit(submitInfo, nullptr); - graphicsQueue.waitIdle(); + // Use mutex to ensure thread-safe access to graphics queue + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } } catch (const std::exception& e) { std::cerr << "Failed to copy buffer: " << e.what() << std::endl; throw; @@ -752,8 +756,12 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima .pCommandBuffers = &*commandBuffer }; - graphicsQueue.submit(submitInfo, nullptr); - graphicsQueue.waitIdle(); + // Use mutex to ensure thread-safe access to graphics queue + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } } catch (const std::exception& e) { std::cerr << "Failed to transition image layout: " << e.what() << std::endl; throw; @@ -812,8 +820,12 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi .pCommandBuffers = &*commandBuffer }; - graphicsQueue.submit(submitInfo, nullptr); - graphicsQueue.waitIdle(); + // Use mutex to ensure thread-safe access to graphics queue + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } } catch (const std::exception& e) { std::cerr << "Failed to copy buffer to image: " << e.what() << std::endl; throw; diff --git a/attachments/simple_engine/shaders/hrtf.slang b/attachments/simple_engine/shaders/hrtf.slang index e55a98d4..f734b106 100644 --- a/attachments/simple_engine/shaders/hrtf.slang +++ b/attachments/simple_engine/shaders/hrtf.slang @@ -7,20 +7,22 @@ [[vk::binding(2, 0)]] StructuredBuffer hrtfData; // HRTF impulse responses [[vk::binding(3, 0)]] ConstantBuffer params; // HRTF parameters -// Parameters for HRTF processing +// Parameters for HRTF processing - MUST match CPU GPUHRTFParams structure exactly struct HRTFParams { - float3 listenerPosition; // Position of the listener - float3 listenerForward; // Forward direction of the listener - float3 listenerUp; // Up direction of the listener - float3 sourcePosition; // Position of the sound source - float sampleRate; // Audio sample rate - uint inputChannels; // Number of input channels (typically 1 or 2) - uint outputChannels; // Number of output channels (typically 2 for stereo) - uint hrtfSize; // Size of each HRTF impulse response - uint numHrtfPositions; // Number of HRTF positions in the dataset - float distanceAttenuation; // Distance attenuation factor - float dopplerFactor; // Doppler effect factor - float reverbMix; // Reverb mix factor + float4 listenerPosition; // Position of the listener (float[4] on CPU) - 16 bytes + float4 listenerForward; // Forward direction of the listener (float[4] on CPU) - 16 bytes + float4 listenerUp; // Up direction of the listener (float[4] on CPU) - 16 bytes + float4 sourcePosition; // Position of the sound source (float[4] on CPU) - 16 bytes + float sampleCount; // Number of samples to process (4 bytes) - offset 64 + float3 padding1; // Padding to align to 16-byte boundary (12 bytes) - offset 68 + uint inputChannels; // Number of input channels (4 bytes) - offset 80 + uint outputChannels; // Number of output channels (4 bytes) - offset 84 + uint hrtfSize; // Size of each HRTF impulse response (4 bytes) - offset 88 + uint numHrtfPositions; // Number of HRTF positions (4 bytes) - offset 92 + float distanceAttenuation; // Distance attenuation factor (4 bytes) - offset 96 + float dopplerFactor; // Doppler effect factor (4 bytes) - offset 100 + float reverbMix; // Reverb mix factor (4 bytes) - offset 104 + float padding2; // Padding to complete 16-byte alignment (4 bytes) - offset 108 }; // Helper function to calculate the index of the closest HRTF in the dataset @@ -46,79 +48,199 @@ uint FindClosestHRTF(float azimuth, float elevation) { // Helper function to calculate azimuth and elevation angles void CalculateAngles(float3 sourceDir, float3 listenerForward, float3 listenerUp, out float azimuth, out float elevation) { - // Calculate listener's right vector - float3 listenerRight = cross(listenerForward, listenerUp); + // Simplified angle calculation - directly use source direction + // Calculate azimuth (horizontal angle) - angle around Y axis + azimuth = atan2(sourceDir.x, -sourceDir.z) * 57.2957795; // Convert to degrees, negate z for correct orientation - // Create rotation matrix from listener's orientation - float3x3 rotation = float3x3( - listenerRight, - listenerUp, - listenerForward - ); - - // Transform source direction to listener's local space - float3 localDir = mul(rotation, sourceDir); - - // Calculate azimuth (horizontal angle) - azimuth = atan2(localDir.x, localDir.z) * 57.2957795; // Convert to degrees - - // Calculate elevation (vertical angle) - float horizontalLength = sqrt(localDir.x * localDir.x + localDir.z * localDir.z); - elevation = atan2(localDir.y, horizontalLength) * 57.2957795; // Convert to degrees + // Calculate elevation (vertical angle) - angle from horizontal plane + float horizontalLength = sqrt(sourceDir.x * sourceDir.x + sourceDir.z * sourceDir.z); + elevation = atan2(sourceDir.y, horizontalLength) * 57.2957795; // Convert to degrees } // Main compute shader function [shader("compute")] [numthreads(64, 1, 1)] -void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) { +void main(uint3 dispatchThreadID : SV_DispatchThreadID) { uint index = dispatchThreadID.x; // Check if the thread is within bounds - if (index >= params.sampleRate) { + if (index >= uint(params.sampleCount)) { return; } - // Calculate direction from listener to source - float3 sourceDir = params.sourcePosition - params.listenerPosition; + // STAGE 1: HRTF DATA ACCESS WITH SAFETY VALIDATION + // Start with working basic panning and add HRTF data access + + // Get input sample for this thread + float inputSample = inputAudioBuffer[index]; + + // STAGE 1: Test HRTF data buffer access with ultra-safe bounds checking + bool hrtfDataValid = false; + float testHrtfSample = 0.0f; + + // Ultra-safe HRTF data access test + if (params.hrtfSize > 0 && params.numHrtfPositions > 0) { + // Test access to first HRTF sample with multiple safety checks + uint testHrtfIndex = 0; // Start with first sample + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; // 2 channels + + if (testHrtfIndex < maxHrtfBufferSize && testHrtfIndex < 500000) { // Additional hardcoded safety limit + testHrtfSample = hrtfData[testHrtfIndex]; + hrtfDataValid = true; + } + } + + // STAGE 2: 3D DIRECTION CALCULATION AND ANGLE COMPUTATION + // Calculate 3D direction from listener to source + float3 sourceDir = params.sourcePosition.xyz - params.listenerPosition.xyz; float distance = length(sourceDir); - sourceDir = normalize(sourceDir); - // Calculate azimuth and elevation angles + // Handle edge case where listener and source are at same position + if (distance < 0.001) { + sourceDir = float3(0.0, 0.0, -1.0); // Default to front direction + distance = 1.0; + } else { + sourceDir = normalize(sourceDir); + } + + // Calculate azimuth and elevation angles using the helper function float azimuth, elevation; - CalculateAngles(sourceDir, params.listenerForward, params.listenerUp, azimuth, elevation); + CalculateAngles(sourceDir, params.listenerForward.xyz, params.listenerUp.xyz, azimuth, elevation); + + + // ENHANCED SPATIAL PROCESSING: Use 3D angles for better panning + float leftGain = 1.0; + float rightGain = 1.0; + + // Convert azimuth to left/right panning (-180 to +180 degrees) + // Positive azimuth = right side, negative = left side + if (azimuth > 0.0) { + // Source is to the right, reduce left channel based on angle + float rightness = min(1.0, azimuth / 90.0); // Normalize to 0-1 for 0-90 degrees + leftGain = max(0.2, 1.0 - rightness * 0.8); // Reduce left by up to 80% + rightGain = 1.0; + } else if (azimuth < 0.0) { + // Source is to the left, reduce right channel based on angle + float leftness = min(1.0, -azimuth / 90.0); // Normalize to 0-1 for 0-90 degrees + leftGain = 1.0; + rightGain = max(0.2, 1.0 - leftness * 0.8); // Reduce right by up to 80% + } - // Find the closest HRTF in the dataset - uint hrtfIndex = FindClosestHRTF(azimuth, elevation); + // Apply distance attenuation (closer sources are louder) + float distanceAttenuation = 1.0 / max(1.0, distance * 0.5); // Gentle distance falloff + leftGain *= distanceAttenuation; + rightGain *= distanceAttenuation; - // Apply distance attenuation - float attenuation = 1.0 / max(1.0, distance * params.distanceAttenuation); + // STAGE 3: HRTF INDEX LOOKUP WITH BOUNDS CHECKING + // Find the closest HRTF in the dataset based on calculated angles + uint hrtfIndex = FindClosestHRTF(azimuth, elevation); - // Process audio for left and right ears - for (uint channel = 0; channel < params.outputChannels; channel++) { - float sum = 0.0; + // Ultra-safe bounds checking for HRTF index + bool hrtfIndexValid = false; + if (hrtfIndex < params.numHrtfPositions && params.numHrtfPositions > 0) { + hrtfIndexValid = true; + } - // Convolve input audio with HRTF impulse response - for (uint i = 0; i < params.hrtfSize; i++) { - if (index + i < params.sampleRate) { - // Get the HRTF sample for this channel and position - uint hrtfOffset = hrtfIndex * params.hrtfSize * params.outputChannels + channel * params.hrtfSize + i; - float hrtfSample = hrtfData[hrtfOffset]; + // ENHANCED HRTF DATA ACCESS: Use calculated index instead of just first sample + float hrtfLeftSample = 0.0f; + float hrtfRightSample = 0.0f; + bool hrtfSamplesValid = false; + + if (hrtfIndexValid && hrtfDataValid) { + // Calculate HRTF buffer offsets for left and right channels + // HRTF data layout: [position0_left_samples][position0_right_samples][position1_left_samples]... + uint leftChannelOffset = hrtfIndex * params.hrtfSize * 2; // 2 channels per position + uint rightChannelOffset = leftChannelOffset + params.hrtfSize; + + // Ultra-safe bounds checking for HRTF sample access + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; + if (leftChannelOffset < maxHrtfBufferSize && rightChannelOffset < maxHrtfBufferSize && + leftChannelOffset < 500000 && rightChannelOffset < 500000) { // Additional hardcoded safety + + // Access first sample of each channel's impulse response for this position + hrtfLeftSample = hrtfData[leftChannelOffset]; + hrtfRightSample = hrtfData[rightChannelOffset]; + hrtfSamplesValid = true; + } + } - // Get the input audio sample - float audioSample = 0.0; - if (index >= i) { - audioSample = inputAudioBuffer[index - i]; + // STAGE 4: HRTF CONVOLUTION LOOP WITH ULTRA-SAFE MEMORY ACCESS + float leftConvolution = 0.0f; + float rightConvolution = 0.0f; + uint convolutionSamples = 0; + + if (hrtfIndexValid && hrtfDataValid && params.hrtfSize > 0) { + // Calculate base offsets for this HRTF position + uint leftChannelBase = hrtfIndex * params.hrtfSize * 2; + uint rightChannelBase = leftChannelBase + params.hrtfSize; + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; + + // Limit convolution size for safety and performance + uint safeHrtfSize = min(params.hrtfSize, 32u); // Limit to 32 samples for safety + + // HRTF Convolution loop with ultra-safe bounds checking + for (uint i = 0; i < safeHrtfSize; i++) { + // Check if we can access the input audio sample + if (index >= i) { + uint inputIndex = index - i; + + // Ultra-safe input buffer bounds check + if (inputIndex < uint(params.sampleCount) && inputIndex < 1024) { + float audioSample = inputAudioBuffer[inputIndex]; + + // Calculate HRTF sample indices with bounds checking + uint leftHrtfIndex = leftChannelBase + i; + uint rightHrtfIndex = rightChannelBase + i; + + // Ultra-safe HRTF buffer bounds check + if (leftHrtfIndex < maxHrtfBufferSize && rightHrtfIndex < maxHrtfBufferSize && + leftHrtfIndex < 500000 && rightHrtfIndex < 500000) { + + float leftHrtfSample = hrtfData[leftHrtfIndex]; + float rightHrtfSample = hrtfData[rightHrtfIndex]; + + // Apply convolution + leftConvolution += audioSample * leftHrtfSample; + rightConvolution += audioSample * rightHrtfSample; + convolutionSamples++; + } } - - // Apply convolution - sum += audioSample * hrtfSample; } } - // Apply distance attenuation - sum *= attenuation; + } - // Write to output buffer - outputAudioBuffer[index * params.outputChannels + channel] = sum; + // STAGE 4: Apply convolution results with distance attenuation + if (convolutionSamples > 0) { + // Use convolution results instead of simple gain modification + leftGain = leftConvolution * distanceAttenuation; + rightGain = rightConvolution * distanceAttenuation; } + + + // STAGE 5: COMPLETE HRTF PROCESSING - FINAL OUTPUT WITH OPTIMIZATION + // Write to both output channels with full HRTF processing + for (uint channel = 0; channel < 2; channel++) { // Hardcode to 2 channels for safety + uint outputIndex = index * 2 + channel; + + // Ultra-safe bounds check with hardcoded limits + if (outputIndex < 1024 * 2 && outputIndex < 2048) { + float finalSample = 0.0f; + + if (convolutionSamples > 0) { + // STAGE 5: Use full HRTF convolution results + finalSample = (channel == 0) ? leftGain : rightGain; + + // Apply output normalization to prevent clipping + finalSample = clamp(finalSample, -1.0f, 1.0f); + } else { + // Fallback: Enhanced spatial panning + float channelGain = (channel == 0) ? leftGain : rightGain; + finalSample = inputSample * channelGain; + } + + outputAudioBuffer[outputIndex] = finalSample; + } + } + } From 86c396a3e38b254bffaf1ce77173cd6fe0053641 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 25 Jul 2025 01:07:44 -0700 Subject: [PATCH 029/102] The bistro scene now loads. Still have some issues with PBR but I think that's due to material only looking at the vespa. This includes camera controls. --- attachments/simple_engine/CMakeLists.txt | 1 + attachments/simple_engine/audio_system.h | 4 - .../simple_engine/camera_component.cpp | 3 - attachments/simple_engine/camera_component.h | 11 + .../simple_engine/descriptor_manager.h | 4 - attachments/simple_engine/engine.cpp | 144 +++ attachments/simple_engine/engine.h | 29 + attachments/simple_engine/imgui_system.cpp | 16 + attachments/simple_engine/imgui_system.h | 13 +- attachments/simple_engine/main.cpp | 46 +- attachments/simple_engine/mesh_component.cpp | 11 + attachments/simple_engine/mesh_component.h | 20 +- attachments/simple_engine/model_loader.cpp | 914 +++++++++++++++--- attachments/simple_engine/model_loader.h | 115 ++- attachments/simple_engine/physics_system.cpp | 26 +- attachments/simple_engine/physics_system.h | 4 - attachments/simple_engine/pipeline.h | 4 - attachments/simple_engine/renderer.h | 49 +- attachments/simple_engine/renderer_core.cpp | 7 +- .../simple_engine/renderer_rendering.cpp | 172 +++- .../simple_engine/renderer_resources.cpp | 371 +++++-- attachments/simple_engine/scene_loading.cpp | 122 +++ attachments/simple_engine/scene_loading.h | 38 + attachments/simple_engine/swap_chain.h | 4 - attachments/simple_engine/vulkan_device.h | 4 - attachments/simple_engine/vulkan_dispatch.cpp | 4 - 26 files changed, 1873 insertions(+), 263 deletions(-) create mode 100644 attachments/simple_engine/scene_loading.cpp create mode 100644 attachments/simple_engine/scene_loading.h diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 53cc5cc7..bef18179 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -89,6 +89,7 @@ add_custom_target(shaders DEPENDS ${SHADER_SPVS}) set(SOURCES main.cpp engine.cpp + scene_loading.cpp platform.cpp renderer_core.cpp renderer_rendering.cpp diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index 0084d2d9..fe22ed9a 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -9,11 +9,7 @@ #include #include #include -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include /** diff --git a/attachments/simple_engine/camera_component.cpp b/attachments/simple_engine/camera_component.cpp index 012fbbd4..6e73145e 100644 --- a/attachments/simple_engine/camera_component.cpp +++ b/attachments/simple_engine/camera_component.cpp @@ -56,8 +56,5 @@ void CameraComponent::UpdateProjectionMatrix() { float halfHeight = orthoHeight * 0.5f; projectionMatrix = glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, nearPlane, farPlane); } - // Flip Y-axis for Vulkan coordinate system - // @see en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc#coordinate-system-differences - projectionMatrix[1][1] *= -1; projectionMatrixDirty = false; } diff --git a/attachments/simple_engine/camera_component.h b/attachments/simple_engine/camera_component.h index 9fd05834..6c6a33c1 100644 --- a/attachments/simple_engine/camera_component.h +++ b/attachments/simple_engine/camera_component.h @@ -150,6 +150,17 @@ class CameraComponent : public Component { viewMatrixDirty = true; } + /** + * @brief Make the camera look at a specific target position. + * @param targetPosition The position to look at. + * @param upVector The up vector (optional, defaults to current up vector). + */ + void LookAt(const glm::vec3& targetPosition, const glm::vec3& upVector = glm::vec3(0.0f, 1.0f, 0.0f)) { + target = targetPosition; + up = upVector; + viewMatrixDirty = true; + } + /** * @brief Get the view matrix. * @return The view matrix. diff --git a/attachments/simple_engine/descriptor_manager.h b/attachments/simple_engine/descriptor_manager.h index c66fc5cb..cd6569ec 100644 --- a/attachments/simple_engine/descriptor_manager.h +++ b/attachments/simple_engine/descriptor_manager.h @@ -1,10 +1,6 @@ #pragma once -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include #include #define GLM_FORCE_RADIANS diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 34fea6bc..df605ab9 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -1,8 +1,10 @@ #include "engine.h" +#include "scene_loading.h" #include #include #include +#include // This implementation corresponds to the Engine_Architecture chapter in the tutorial: // @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -38,6 +40,37 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Set mouse callback platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { + // Handle camera rotation when left mouse button is pressed + if (buttons & 1) { // Left mouse button (bit 0) + if (!cameraControl.mouseLeftPressed) { + cameraControl.mouseLeftPressed = true; + cameraControl.firstMouse = true; + } + + if (cameraControl.firstMouse) { + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + cameraControl.firstMouse = false; + } + + float xOffset = x - cameraControl.lastMouseX; + float yOffset = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + + xOffset *= cameraControl.mouseSensitivity; + yOffset *= cameraControl.mouseSensitivity; + + cameraControl.yaw += xOffset; + cameraControl.pitch += yOffset; + + // Constrain pitch to avoid gimbal lock + if (cameraControl.pitch > 89.0f) cameraControl.pitch = 89.0f; + if (cameraControl.pitch < -89.0f) cameraControl.pitch = -89.0f; + } else { + cameraControl.mouseLeftPressed = false; + } + if (imguiSystem) { imguiSystem->HandleMouse(x, y, buttons); } @@ -45,6 +78,34 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Set keyboard callback platform->SetKeyboardCallback([this](uint32_t key, bool pressed) { + // Handle camera movement keys (WASD + Arrow keys) + switch (key) { + case GLFW_KEY_W: + case GLFW_KEY_UP: + cameraControl.moveForward = pressed; + break; + case GLFW_KEY_S: + case GLFW_KEY_DOWN: + cameraControl.moveBackward = pressed; + break; + case GLFW_KEY_A: + case GLFW_KEY_LEFT: + cameraControl.moveLeft = pressed; + break; + case GLFW_KEY_D: + case GLFW_KEY_RIGHT: + cameraControl.moveRight = pressed; + break; + case GLFW_KEY_Q: + case GLFW_KEY_PAGE_UP: + cameraControl.moveUp = pressed; + break; + case GLFW_KEY_E: + case GLFW_KEY_PAGE_DOWN: + cameraControl.moveDown = pressed; + break; + } + if (imguiSystem) { imguiSystem->HandleKeyboard(key, pressed); } @@ -68,6 +129,9 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool return false; } + // Connect model loader to renderer for light extraction + renderer->SetModelLoader(modelLoader.get()); + // Initialize audio system if (!audioSystem->Initialize(renderer.get())) { return false; @@ -246,6 +310,9 @@ ImGuiSystem* Engine::GetImGuiSystem() const { } void Engine::Update(float deltaTime) { + // Check for completed background loading and create entities if ready + CheckAndCreateLoadedEntities(); + // Update physics system physicsSystem->Update(deltaTime); @@ -255,6 +322,11 @@ void Engine::Update(float deltaTime) { // Update ImGui system imguiSystem->NewFrame(); + // Update camera controls + if (activeCamera) { + UpdateCameraControls(deltaTime); + } + // Update all entities for (auto& entity : entities) { if (entity->IsActive()) { @@ -325,6 +397,75 @@ void Engine::HandleResize(int width, int height) { } } +void Engine::UpdateCameraControls(float deltaTime) { + if (!activeCamera) return; + + // Get camera transform component + TransformComponent* cameraTransform = activeCamera->GetOwner()->GetComponent(); + if (!cameraTransform) return; + + // Calculate movement speed + float velocity = cameraControl.cameraSpeed * deltaTime; + + // Calculate camera direction vectors based on yaw and pitch + glm::vec3 front; + front.x = cos(glm::radians(cameraControl.yaw)) * cos(glm::radians(cameraControl.pitch)); + front.y = sin(glm::radians(cameraControl.pitch)); + front.z = sin(glm::radians(cameraControl.yaw)) * cos(glm::radians(cameraControl.pitch)); + front = glm::normalize(front); + + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); + glm::vec3 right = glm::normalize(glm::cross(front, up)); + up = glm::normalize(glm::cross(right, front)); + + // Get current camera position + glm::vec3 position = cameraTransform->GetPosition(); + + // Apply movement based on input + if (cameraControl.moveForward) { + position += front * velocity; + } + if (cameraControl.moveBackward) { + position -= front * velocity; + } + if (cameraControl.moveLeft) { + position -= right * velocity; + } + if (cameraControl.moveRight) { + position += right * velocity; + } + if (cameraControl.moveUp) { + position += up * velocity; + } + if (cameraControl.moveDown) { + position -= up * velocity; + } + + // Update camera position + cameraTransform->SetPosition(position); + + // Update camera target based on direction + glm::vec3 target = position + front; + activeCamera->SetTarget(target); +} + +void Engine::CheckAndCreateLoadedEntities() { + // Check if background loading is complete + if (g_loadingState.loadingComplete && !g_loadingState.loadedMaterials.empty()) { + // Create entities from loaded materials on the main thread + CreateEntitiesFromLoadedMaterials(this); + + // Reset the loading complete flag + g_loadingState.loadingComplete = false; + } + + // Check for loading errors + if (g_loadingState.loadingFailed) { + std::cerr << "Background loading failed: " << g_loadingState.errorMessage << std::endl; + g_loadingState.loadingFailed = false; // Reset the flag + } +} + #if PLATFORM_ANDROID // Android-specific implementation bool Engine::InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers) { @@ -371,6 +512,9 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo return false; } + // Connect model loader to renderer for light extraction + renderer->SetModelLoader(modelLoader.get()); + // Initialize audio system if (!audioSystem->Initialize(renderer.get())) { return false; diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index b9dcba93..cadd5d1e 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -182,6 +182,24 @@ class Engine { float deltaTime = 0.0f; uint64_t lastFrameTime = 0; + // Camera control state + struct CameraControlState { + bool moveForward = false; + bool moveBackward = false; + bool moveLeft = false; + bool moveRight = false; + bool moveUp = false; + bool moveDown = false; + bool mouseLeftPressed = false; + float lastMouseX = 0.0f; + float lastMouseY = 0.0f; + float yaw = 0.0f; // Horizontal rotation + float pitch = 0.0f; // Vertical rotation + bool firstMouse = true; + float cameraSpeed = 5.0f; + float mouseSensitivity = 0.1f; + } cameraControl; + /** * @brief Update the engine state. * @param deltaTime The time elapsed since the last update. @@ -205,4 +223,15 @@ class Engine { * @param height The new height of the window. */ void HandleResize(int width, int height); + + /** + * @brief Update camera controls based on input state. + * @param deltaTime The time elapsed since the last update. + */ + void UpdateCameraControls(float deltaTime); + + /** + * @brief Check for completed background loading and create entities if ready. + */ + void CheckAndCreateLoadedEntities(); }; diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index cd7ca07f..c293f97e 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -109,6 +109,22 @@ void ImGuiSystem::NewFrame() { // Create HRTF Audio Control UI ImGui::Begin("HRTF Audio Controls"); ImGui::Text("Hello, Vulkan!"); + // PBR Rendering Controls + ImGui::Separator(); + ImGui::Text("PBR Rendering Controls:"); + + if (ImGui::Checkbox("Enable PBR Rendering", &pbrEnabled)) { + std::cout << "PBR rendering " << (pbrEnabled ? "enabled" : "disabled") << std::endl; + } + + if (pbrEnabled) { + ImGui::Text("Status: PBR pipeline active for supported models"); + ImGui::Text("Models using PBR: Bistro scene"); + } else { + ImGui::Text("Status: Using basic rendering pipeline"); + ImGui::Text("All models rendered with basic shading"); + } + ImGui::Text("3D Audio Position Control"); // Audio source selection diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index 6610f57d..56f16406 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -3,11 +3,7 @@ #include #include #include -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include // Forward declarations @@ -105,6 +101,12 @@ class ImGuiSystem { */ void SetAudioSystem(AudioSystem* audioSystem); + /** + * @brief Get the current PBR rendering state. + * @return True if PBR rendering is enabled, false otherwise. + */ + bool IsPBREnabled() const { return pbrEnabled; } + private: // ImGui context ImGuiContext* context = nullptr; @@ -151,6 +153,9 @@ class ImGuiSystem { // Initialization flag bool initialized = false; + // PBR rendering state + bool pbrEnabled = false; + /** * @brief Create Vulkan resources for ImGui. * @return True if creation was successful, false otherwise. diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index b6ec0bae..b07abd5d 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -2,15 +2,18 @@ #include "transform_component.h" #include "mesh_component.h" #include "camera_component.h" +#include "scene_loading.h" #include #include +#include // Constants constexpr int WINDOW_WIDTH = 800; constexpr int WINDOW_HEIGHT = 600; constexpr bool ENABLE_VALIDATION_LAYERS = true; + /** * @brief Set up a simple scene with a camera and some objects. * @param engine The engine to set up the scene in. @@ -22,12 +25,12 @@ void SetupScene(Engine* engine) { throw std::runtime_error("Failed to create camera entity"); } - // Add transform component to the camera - TransformComponent* cameraTransform = cameraEntity->AddComponent(); + // Add a transform component to the camera + auto* cameraTransform = cameraEntity->AddComponent(); cameraTransform->SetPosition(glm::vec3(0.0f, 0.0f, 3.0f)); - // Add camera component to the camera entity - CameraComponent* camera = cameraEntity->AddComponent(); + // Add a camera component to the camera entity + auto* camera = cameraEntity->AddComponent(); camera->SetAspectRatio(static_cast(WINDOW_WIDTH) / static_cast(WINDOW_HEIGHT)); // Set the camera as the active camera @@ -39,30 +42,33 @@ void SetupScene(Engine* engine) { throw std::runtime_error("Failed to create cube entity"); } - // Add transform component to the cube - TransformComponent* cubeTransform = cubeEntity->AddComponent(); + // Add a transform component to the cube + auto* cubeTransform = cubeEntity->AddComponent(); cubeTransform->SetPosition(glm::vec3(0.0f, 0.0f, 0.0f)); cubeTransform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); cubeTransform->SetScale(glm::vec3(1.0f, 1.0f, 1.0f)); - // Add mesh component to the cube - MeshComponent* cubeMesh = cubeEntity->AddComponent(); + // Add a mesh component to the cube + auto* cubeMesh = cubeEntity->AddComponent(); cubeMesh->CreateCube(1.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + // Make the camera look at the red cube + camera->LookAt(cubeTransform->GetPosition()); + // Create a second cube entity Entity* cube2Entity = engine->CreateEntity("Cube2"); if (!cube2Entity) { throw std::runtime_error("Failed to create second cube entity"); } - // Add transform component to the second cube - TransformComponent* cube2Transform = cube2Entity->AddComponent(); + // Add a transform component to the second cube + auto* cube2Transform = cube2Entity->AddComponent(); cube2Transform->SetPosition(glm::vec3(2.0f, 0.0f, 0.0f)); cube2Transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); cube2Transform->SetScale(glm::vec3(0.5f, 0.5f, 0.5f)); - // Add mesh component to the second cube - MeshComponent* cube2Mesh = cube2Entity->AddComponent(); + // Add a mesh component to the second cube + auto* cube2Mesh = cube2Entity->AddComponent(); cube2Mesh->CreateCube(1.0f, glm::vec3(0.0f, 1.0f, 0.0f)); // Create a third cube entity @@ -71,15 +77,23 @@ void SetupScene(Engine* engine) { throw std::runtime_error("Failed to create third cube entity"); } - // Add transform component to the third cube - TransformComponent* cube3Transform = cube3Entity->AddComponent(); + // Add a transform component to the third cube + auto* cube3Transform = cube3Entity->AddComponent(); cube3Transform->SetPosition(glm::vec3(-2.0f, 0.0f, 0.0f)); cube3Transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); cube3Transform->SetScale(glm::vec3(0.5f, 0.5f, 0.5f)); - // Add mesh component to the third cube - MeshComponent* cube3Mesh = cube3Entity->AddComponent(); + // Add a mesh component to the third cube + auto* cube3Mesh = cube3Entity->AddComponent(); cube3Mesh->CreateCube(1.0f, glm::vec3(0.0f, 0.0f, 1.0f)); + + // Start loading Bistro.glb model in background thread + if (ModelLoader* modelLoader = engine->GetModelLoader()) { + std::cout << "Starting threaded loading of Bistro model..." << std::endl; + std::thread loadingThread(LoadBistroModelAsync, modelLoader); + loadingThread.detach(); // Let the thread run independently + std::cout << "Background loading thread started. Application will continue running..." << std::endl; + } } #if PLATFORM_ANDROID diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index a35c70e7..b9ca63a2 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -1,4 +1,5 @@ #include "mesh_component.h" +#include "model_loader.h" // Most of the MeshComponent class implementation is in the header file // This file is mainly for any methods that might need additional implementation @@ -76,3 +77,13 @@ void MeshComponent::CreateCube(float size, const glm::vec3& color) { 20, 21, 22, 22, 23, 20 }; } + +void MeshComponent::LoadFromModel(const Model* model) { + if (!model) { + return; + } + + // Copy vertex and index data from the model + vertices = model->GetVertices(); + indices = model->GetIndices(); +} diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index a5088cd5..6d7fd24d 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -5,11 +5,7 @@ #include #include -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include "component.h" @@ -28,11 +24,11 @@ struct Vertex { } static vk::VertexInputBindingDescription getBindingDescription() { - vk::VertexInputBindingDescription bindingDescription{ - .binding = 0, - .stride = sizeof(Vertex), - .inputRate = vk::VertexInputRate::eVertex - }; + vk::VertexInputBindingDescription bindingDescription( + 0, // binding + sizeof(Vertex), // stride + vk::VertexInputRate::eVertex // inputRate + ); return bindingDescription; } @@ -143,4 +139,10 @@ class MeshComponent : public Component { * @param color The color of the cube. */ void CreateCube(float size = 1.0f, const glm::vec3& color = glm::vec3(1.0f)); + + /** + * @brief Load mesh data from a Model. + * @param model Pointer to the model to load from. + */ + void LoadFromModel(const class Model* model); }; diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index a3a0e2b8..9dccb1fc 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -1,20 +1,14 @@ #include "model_loader.h" #include "renderer.h" +#include "mesh_component.h" #include #include +#include -// Forward declarations for classes that will be defined in separate files -class Model { -public: - Model(const std::string& name) : name(name) {} - ~Model() = default; +// Include stb_image for proper texture loading (implementation is in renderer_resources.cpp) +#include - const std::string& GetName() const { return name; } - -private: - std::string name; - // Other model data (meshes, materials, etc.) -}; +// Forward declarations for classes that will be defined in separate files class Material { public: @@ -30,9 +24,15 @@ class Material { float ao = 1.0f; glm::vec3 emissive = glm::vec3(0.0f); + // Texture paths for PBR materials + std::string albedoTexturePath; + std::string normalTexturePath; + std::string metallicRoughnessTexturePath; + std::string occlusionTexturePath; + std::string emissiveTexturePath; + private: std::string name; - // Texture references }; ModelLoader::ModelLoader() { @@ -160,61 +160,431 @@ Material* ModelLoader::CreatePBRMaterial(const std::string& name, } bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { - // Parse the GLTF file and populate the model std::cout << "Parsing GLTF file: " << filename << std::endl; - // Open the file - std::ifstream file(filename, std::ios::binary); - if (!file.is_open()) { - std::cerr << "Failed to open GLTF file: " << filename << std::endl; + // Create tinygltf loader + tinygltf::Model gltfModel; + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + // Set up a proper image loader callback using stb_image + loader.SetImageLoader([](tinygltf::Image* image, const int image_idx, std::string* err, + std::string* warn, int req_width, int req_height, + const unsigned char* bytes, int size, void* user_data) -> bool { + // Use stb_image to decode the image data + int width, height, channels; + unsigned char* data = stbi_load_from_memory(bytes, size, &width, &height, &channels, 0); + + if (!data) { + if (err) { + *err = "Failed to load image with stb_image: " + std::string(stbi_failure_reason()); + } + return false; + } + + // Set image properties + image->width = width; + image->height = height; + image->component = channels; + image->bits = 8; + image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + + // Copy image data + size_t image_size = width * height * channels; + image->image.resize(image_size); + std::memcpy(image->image.data(), data, image_size); + + // Free stb_image data + stbi_image_free(data); + + std::cout << "Loaded texture: " << width << "x" << height << " with " << channels << " channels" << std::endl; + return true; + }, nullptr); + + // Load the GLTF file + bool ret = false; + if (filename.find(".glb") != std::string::npos) { + ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, filename); + } else { + ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, filename); + } + + if (!warn.empty()) { + std::cout << "GLTF Warning: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "GLTF Error: " << err << std::endl; return false; } - // Read the file content - file.seekg(0, std::ios::end); - size_t fileSize = file.tellg(); - file.seekg(0, std::ios::beg); + if (!ret) { + std::cerr << "Failed to parse GLTF file: " << filename << std::endl; + return false; + } - std::vector buffer(fileSize); - file.read(buffer.data(), fileSize); - file.close(); + std::cout << "Successfully loaded GLTF file with " << gltfModel.meshes.size() << " meshes" << std::endl; - // Parse the JSON content - // In a real implementation, this would use a JSON library like nlohmann/json - // For simplicity, we'll just check if the file contains the required GLTF header - std::string content(buffer.begin(), buffer.end()); - if (content.find("\"asset\"") == std::string::npos || - content.find("\"version\"") == std::string::npos) { - std::cerr << "Invalid GLTF file format: " << filename << std::endl; + // Extract mesh data from the first mesh (for now, we'll handle multiple meshes later) + if (gltfModel.meshes.empty()) { + std::cerr << "No meshes found in GLTF file" << std::endl; return false; } - // Extract mesh data - // In a real implementation, this would parse the full GLTF structure - // For now, we'll just log what we would extract - std::cout << "Extracting mesh data from GLTF file" << std::endl; - std::cout << " - Vertices" << std::endl; - std::cout << " - Indices" << std::endl; - std::cout << " - Normals" << std::endl; - std::cout << " - Texture coordinates" << std::endl; - std::cout << " - Materials" << std::endl; - - // Extract animation data if present - if (content.find("\"animations\"") != std::string::npos) { - std::cout << "Extracting animation data from GLTF file" << std::endl; - std::cout << " - Keyframes" << std::endl; - std::cout << " - Bone weights" << std::endl; - std::cout << " - Animation channels" << std::endl; - } - - // Extract scene hierarchy - std::cout << "Extracting scene hierarchy from GLTF file" << std::endl; - std::cout << " - Nodes" << std::endl; - std::cout << " - Node hierarchy" << std::endl; - std::cout << " - Node transforms" << std::endl; - - // In a real implementation, we would create meshes, materials, and set up the model - // For now, we'll just return success + // Process materials first + std::cout << "Processing " << gltfModel.materials.size() << " materials" << std::endl; + for (size_t i = 0; i < gltfModel.materials.size(); ++i) { + const auto& gltfMaterial = gltfModel.materials[i]; + std::cout << " Material " << i << ": " << gltfMaterial.name << std::endl; + + // Create PBR material + auto material = std::make_unique(gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name); + + // Extract PBR properties + if (gltfMaterial.pbrMetallicRoughness.baseColorFactor.size() >= 3) { + material->albedo = glm::vec3( + gltfMaterial.pbrMetallicRoughness.baseColorFactor[0], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[1], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[2] + ); + } + material->metallic = gltfMaterial.pbrMetallicRoughness.metallicFactor; + material->roughness = gltfMaterial.pbrMetallicRoughness.roughnessFactor; + + if (gltfMaterial.emissiveFactor.size() >= 3) { + material->emissive = glm::vec3( + gltfMaterial.emissiveFactor[0], + gltfMaterial.emissiveFactor[1], + gltfMaterial.emissiveFactor[2] + ); + } + + // Extract texture information and load embedded texture data + if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { + int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->albedoTexturePath = textureId; + + // Load embedded texture data + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded base color texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load base color texture: " << textureId << std::endl; + } + } + } + } + } + + if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { + int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->metallicRoughnessTexturePath = textureId; + + // Load embedded texture data + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded metallic-roughness texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load metallic-roughness texture: " << textureId << std::endl; + } + } + } + } + } + + if (gltfMaterial.normalTexture.index >= 0) { + int texIndex = gltfMaterial.normalTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->normalTexturePath = textureId; + + // Load embedded texture data + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded normal texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load normal texture: " << textureId << std::endl; + } + } + } + } + } + + if (gltfMaterial.occlusionTexture.index >= 0) { + int texIndex = gltfMaterial.occlusionTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->occlusionTexturePath = textureId; + + // Load embedded texture data + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded occlusion texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load occlusion texture: " << textureId << std::endl; + } + } + } + } + } + + if (gltfMaterial.emissiveTexture.index >= 0) { + int texIndex = gltfMaterial.emissiveTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->emissiveTexturePath = textureId; + + // Load embedded texture data + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded emissive texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load emissive texture: " << textureId << std::endl; + } + } + } + } + } + + // Store the material + materials[material->GetName()] = std::move(material); + } + + // Group primitives by material to create separate meshes for each material + std::map> materialVertices; + std::map> materialIndices; + std::map materialNames; + + // Process all meshes and group by material + for (const auto& mesh : gltfModel.meshes) { + std::cout << "Processing mesh: " << mesh.name << std::endl; + + for (const auto& primitive : mesh.primitives) { + // Get the material index for this primitive + int materialIndex = primitive.material; + if (materialIndex < 0) { + materialIndex = -1; // Use -1 for primitives without materials + } + + // Initialize vectors for this material if not already done + if (materialVertices.find(materialIndex) == materialVertices.end()) { + materialVertices[materialIndex] = std::vector(); + materialIndices[materialIndex] = std::vector(); + + // Store material name for debugging + if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { + const auto& gltfMaterial = gltfModel.materials[materialIndex]; + materialNames[materialIndex] = gltfMaterial.name.empty() ? + ("material_" + std::to_string(materialIndex)) : gltfMaterial.name; + } else { + materialNames[materialIndex] = "no_material"; + } + + std::cout << " Found material " << materialIndex << ": " << materialNames[materialIndex] << std::endl; + } + // Get indices for this material + if (primitive.indices >= 0) { + const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; + const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; + const tinygltf::Buffer& indexBuffer = gltfModel.buffers[indexBufferView.buffer]; + + const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; + + size_t indexOffset = materialVertices[materialIndex].size(); + + // Handle different index types + if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + const uint16_t* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + materialIndices[materialIndex].push_back(buf[i] + indexOffset); + } + } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + const uint32_t* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + materialIndices[materialIndex].push_back(buf[i] + indexOffset); + } + } + } + + // Get vertex positions + auto posIt = primitive.attributes.find("POSITION"); + if (posIt == primitive.attributes.end()) { + std::cerr << "No POSITION attribute found in primitive" << std::endl; + continue; + } + + const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; + const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; + const tinygltf::Buffer& posBuffer = gltfModel.buffers[posBufferView.buffer]; + + const float* positions = reinterpret_cast( + &posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset]); + + // Get texture coordinates (if available) + const float* texCoords = nullptr; + auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); + if (texCoordIt != primitive.attributes.end()) { + const tinygltf::Accessor& texCoordAccessor = gltfModel.accessors[texCoordIt->second]; + const tinygltf::BufferView& texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; + const tinygltf::Buffer& texCoordBuffer = gltfModel.buffers[texCoordBufferView.buffer]; + texCoords = reinterpret_cast( + &texCoordBuffer.data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]); + } + + // Get normals (if available) + const float* normals = nullptr; + auto normalIt = primitive.attributes.find("NORMAL"); + if (normalIt != primitive.attributes.end()) { + const tinygltf::Accessor& normalAccessor = gltfModel.accessors[normalIt->second]; + const tinygltf::BufferView& normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; + const tinygltf::Buffer& normalBuffer = gltfModel.buffers[normalBufferView.buffer]; + normals = reinterpret_cast( + &normalBuffer.data[normalBufferView.byteOffset + normalAccessor.byteOffset]); + } + + // Create vertices for this material + for (size_t i = 0; i < posAccessor.count; ++i) { + Vertex vertex{}; + + // Position + vertex.position = glm::vec3( + positions[i * 3 + 0], + positions[i * 3 + 1], + positions[i * 3 + 2] + ); + + // Color (use normal as color if available, otherwise white) + if (normals) { + vertex.color = glm::vec3( + std::abs(normals[i * 3 + 0]), + std::abs(normals[i * 3 + 1]), + std::abs(normals[i * 3 + 2]) + ); + } else { + vertex.color = glm::vec3(0.8f, 0.8f, 0.8f); + } + + // Texture coordinates + if (texCoords) { + vertex.texCoord = glm::vec2( + texCoords[i * 2 + 0], + texCoords[i * 2 + 1] + ); + } else { + vertex.texCoord = glm::vec2(0.0f, 0.0f); + } + + materialVertices[materialIndex].push_back(vertex); + } + } + } + + // Store material meshes and combine for backward compatibility + std::vector modelMaterialMeshes; + std::vector combinedVertices; + std::vector combinedIndices; + + std::cout << "Processing " << materialVertices.size() << " materials:" << std::endl; + + for (const auto& materialPair : materialVertices) { + int materialIndex = materialPair.first; + const auto& vertices = materialPair.second; + const auto& indices = materialIndices[materialIndex]; + + std::cout << " Material " << materialIndex << " (" << materialNames[materialIndex] + << "): " << vertices.size() << " vertices, " << indices.size() << " indices" << std::endl; + + // Create MaterialMesh for this material + MaterialMesh materialMesh; + materialMesh.materialIndex = materialIndex; + materialMesh.materialName = materialNames[materialIndex]; + materialMesh.vertices = vertices; + materialMesh.indices = indices; + + // Get texture path for this material + if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { + const auto& gltfMaterial = gltfModel.materials[materialIndex]; + + // Try to get base color texture first + if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { + int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; + materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + } + // Fall back to other texture types if no base color + else if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { + int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; + materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + } + else if (gltfMaterial.normalTexture.index >= 0) { + int texIndex = gltfMaterial.normalTexture.index; + materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + } + } + + std::cout << " Texture path: " << (materialMesh.texturePath.empty() ? "none" : materialMesh.texturePath) << std::endl; + + modelMaterialMeshes.push_back(materialMesh); + + // Also add to combined mesh for backward compatibility + size_t vertexOffset = combinedVertices.size(); + combinedVertices.insert(combinedVertices.end(), vertices.begin(), vertices.end()); + + for (uint32_t index : indices) { + combinedIndices.push_back(index + vertexOffset); + } + } + + // Store material meshes for this model + materialMeshes[filename] = modelMaterialMeshes; + + // Set the combined mesh data in the model for backward compatibility + model->SetVertices(combinedVertices); + model->SetIndices(combinedIndices); + + // Extract lights from the GLTF model + std::cout << "Extracting lights from GLTF model..." << std::endl; + + // Extract punctual lights (KHR_lights_punctual extension) + if (!ExtractPunctualLights(gltfModel, filename)) { + std::cerr << "Warning: Failed to extract punctual lights from " << filename << std::endl; + } + + // Extract lights from emissive materials + if (!ExtractEmissiveLights(gltfModel, filename)) { + std::cerr << "Warning: Failed to extract emissive lights from " << filename << std::endl; + } + + std::cout << "GLTF model loaded successfully with " << combinedVertices.size() << " vertices and " << combinedIndices.size() << " indices" << std::endl; return true; } @@ -241,77 +611,405 @@ bool ModelLoader::LoadPBRTextures(Material* material, // Load albedo map if (!albedoMap.empty()) { std::cout << " Loading albedo map: " << albedoMap << std::endl; - // In a real implementation, this would load the texture using the renderer - // For example: - // VkImage albedoImage = renderer->LoadTexture(albedoMap); - // if (albedoImage != VK_NULL_HANDLE) { - // material->albedoTexture = albedoImage; - // } else { - // std::cerr << "Failed to load albedo map: " << albedoMap << std::endl; - // success = false; - // } + material->albedoTexturePath = albedoMap; + if (!renderer->LoadTexture(albedoMap)) { + std::cerr << " Failed to load albedo texture: " << albedoMap << std::endl; + success = false; + } } // Load normal map if (!normalMap.empty()) { std::cout << " Loading normal map: " << normalMap << std::endl; - // In a real implementation, this would load the texture using the renderer - // For example: - // VkImage normalImage = renderer->LoadTexture(normalMap); - // if (normalImage != VK_NULL_HANDLE) { - // material->normalTexture = normalImage; - // } else { - // std::cerr << "Failed to load normal map: " << normalMap << std::endl; - // success = false; - // } + material->normalTexturePath = normalMap; + if (!renderer->LoadTexture(normalMap)) { + std::cerr << " Failed to load normal texture: " << normalMap << std::endl; + success = false; + } } // Load metallic-roughness map if (!metallicRoughnessMap.empty()) { std::cout << " Loading metallic-roughness map: " << metallicRoughnessMap << std::endl; - // In a real implementation, this would load the texture using the renderer - // For example: - // VkImage metallicRoughnessImage = renderer->LoadTexture(metallicRoughnessMap); - // if (metallicRoughnessImage != VK_NULL_HANDLE) { - // material->metallicRoughnessTexture = metallicRoughnessImage; - // } else { - // std::cerr << "Failed to load metallic-roughness map: " << metallicRoughnessMap << std::endl; - // success = false; - // } + material->metallicRoughnessTexturePath = metallicRoughnessMap; + if (!renderer->LoadTexture(metallicRoughnessMap)) { + std::cerr << " Failed to load metallic-roughness texture: " << metallicRoughnessMap << std::endl; + success = false; + } } // Load ambient occlusion map if (!aoMap.empty()) { std::cout << " Loading ambient occlusion map: " << aoMap << std::endl; - // In a real implementation, this would load the texture using the renderer - // For example: - // VkImage aoImage = renderer->LoadTexture(aoMap); - // if (aoImage != VK_NULL_HANDLE) { - // material->aoTexture = aoImage; - // } else { - // std::cerr << "Failed to load ambient occlusion map: " << aoMap << std::endl; - // success = false; - // } + material->occlusionTexturePath = aoMap; + if (!renderer->LoadTexture(aoMap)) { + std::cerr << " Failed to load occlusion texture: " << aoMap << std::endl; + success = false; + } } // Load emissive map if (!emissiveMap.empty()) { std::cout << " Loading emissive map: " << emissiveMap << std::endl; - // In a real implementation, this would load the texture using the renderer - // For example: - // VkImage emissiveImage = renderer->LoadTexture(emissiveMap); - // if (emissiveImage != VK_NULL_HANDLE) { - // material->emissiveTexture = emissiveImage; - // } else { - // std::cerr << "Failed to load emissive map: " << emissiveMap << std::endl; - // success = false; - // } - } - - // Set up PBR material properties - // In a real implementation, this would set up the material properties based on the loaded textures - // For example: - // material->SetupPBRPipeline(); + material->emissiveTexturePath = emissiveMap; + if (!renderer->LoadTexture(emissiveMap)) { + std::cerr << " Failed to load emissive texture: " << emissiveMap << std::endl; + success = false; + } + } + std::cout << "PBR texture paths stored for material: " << material->GetName() << std::endl; return success; } + +std::string ModelLoader::GetFirstMaterialTexturePath(const std::string& modelName) { + // Iterate through all materials to find the first one with an albedo texture path + for (const auto& materialPair : materials) { + const auto& material = materialPair.second; + if (!material->albedoTexturePath.empty()) { + std::cout << "Found texture path for model " << modelName << ": " << material->albedoTexturePath << std::endl; + return material->albedoTexturePath; + } + } + + // If no albedo texture found, try other texture types + for (const auto& materialPair : materials) { + const auto& material = materialPair.second; + if (!material->normalTexturePath.empty()) { + std::cout << "Found normal texture path for model " << modelName << ": " << material->normalTexturePath << std::endl; + return material->normalTexturePath; + } + if (!material->metallicRoughnessTexturePath.empty()) { + std::cout << "Found metallic-roughness texture path for model " << modelName << ": " << material->metallicRoughnessTexturePath << std::endl; + return material->metallicRoughnessTexturePath; + } + } + + std::cout << "No texture path found for model: " << modelName << std::endl; + return ""; +} + +std::vector ModelLoader::GetExtractedLights(const std::string& modelName) const { + auto it = extractedLights.find(modelName); + if (it != extractedLights.end()) { + return it->second; + } + return std::vector(); +} + +std::vector ModelLoader::GetMaterialMeshes(const std::string& modelName) const { + auto it = materialMeshes.find(modelName); + if (it != materialMeshes.end()) { + return it->second; + } + return std::vector(); +} + +bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const std::string& modelName) { + std::cout << "Extracting punctual lights from model: " << modelName << std::endl; + + // Check if the model has the KHR_lights_punctual extension + auto extensionIt = gltfModel.extensions.find("KHR_lights_punctual"); + if (extensionIt == gltfModel.extensions.end()) { + std::cout << " No KHR_lights_punctual extension found" << std::endl; + return true; // Not an error, just no punctual lights + } + + std::cout << " Found KHR_lights_punctual extension" << std::endl; + + // TODO: Parse the punctual lights from the extension + // This would require parsing the JSON structure of the extension + // For now, we'll focus on emissive lights + + return true; +} + +bool ModelLoader::ExtractEmissiveLights(const tinygltf::Model& gltfModel, const std::string& modelName) { + std::cout << "Extracting emissive lights from model: " << modelName << std::endl; + + std::vector lights; + + // Iterate through materials to find emissive ones + for (size_t i = 0; i < gltfModel.materials.size(); ++i) { + const auto& gltfMaterial = gltfModel.materials[i]; + + // Check if material has emissive properties + bool hasEmissiveFactor = gltfMaterial.emissiveFactor.size() >= 3; + bool hasEmissiveTexture = gltfMaterial.emissiveTexture.index >= 0; + + if (!hasEmissiveFactor && !hasEmissiveTexture) { + continue; // No emissive properties + } + + // Calculate emissive intensity + glm::vec3 emissiveColor(0.0f); + if (hasEmissiveFactor) { + emissiveColor = glm::vec3( + gltfMaterial.emissiveFactor[0], + gltfMaterial.emissiveFactor[1], + gltfMaterial.emissiveFactor[2] + ); + } + + // Calculate luminance to determine if this should be a light source + float luminance = 0.299f * emissiveColor.r + 0.587f * emissiveColor.g + 0.114f * emissiveColor.b; + + // Only create lights for materials with significant emissive values + if (luminance > 0.1f) { // Threshold for creating a light + ExtractedLight light; + light.type = ExtractedLight::Type::Emissive; + light.color = emissiveColor; + light.intensity = luminance * 10.0f; // Scale up for lighting + light.range = 50.0f; // Default range for emissive lights + light.sourceMaterial = gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name; + + // For now, place the light at the origin - we'll improve this later + // by calculating positions from mesh geometry + light.position = glm::vec3(0.0f, 5.0f, 0.0f); + + lights.push_back(light); + + std::cout << " Created emissive light from material '" << light.sourceMaterial + << "' with intensity " << light.intensity << std::endl; + } + } + + // Store the extracted lights + extractedLights[modelName] = lights; + + std::cout << " Extracted " << lights.size() << " emissive lights" << std::endl; + return true; +} + +std::vector ModelLoader::ParseGLTFDataOnly(const std::string& filename) { + std::cout << "Thread-safe parsing GLTF file: " << filename << std::endl; + + // Create tinygltf loader + tinygltf::Model gltfModel; + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + // Set up a dummy image loader callback that doesn't load actual image data + loader.SetImageLoader([](tinygltf::Image* image, const int image_idx, std::string* err, + std::string* warn, int req_width, int req_height, + const unsigned char* bytes, int size, void* user_data) -> bool { + // Just set basic image properties without loading actual data + // This avoids any potential file I/O or memory allocation issues in background thread + image->width = 1; + image->height = 1; + image->component = 4; // RGBA + image->bits = 8; + image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + image->image.resize(4, 255); // Dummy white pixel + return true; + }, nullptr); + + // Load the GLTF file + bool ret = false; + if (filename.find(".glb") != std::string::npos) { + ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, filename); + } else { + ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, filename); + } + + if (!warn.empty()) { + std::cout << "GLTF Warning: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "GLTF Error: " << err << std::endl; + return std::vector(); + } + + if (!ret) { + std::cerr << "Failed to parse GLTF file: " << filename << std::endl; + return std::vector(); + } + + std::cout << "Successfully loaded GLTF file with " << gltfModel.meshes.size() << " meshes (thread-safe)" << std::endl; + + // Extract mesh data from the first mesh (for now, we'll handle multiple meshes later) + if (gltfModel.meshes.empty()) { + std::cerr << "No meshes found in GLTF file" << std::endl; + return std::vector(); + } + + // Group primitives by material to create separate meshes for each material + std::map> materialVertices; + std::map> materialIndices; + std::map materialNames; + + // Process all meshes and group by material + for (const auto& mesh : gltfModel.meshes) { + std::cout << "Processing mesh: " << mesh.name << " (thread-safe)" << std::endl; + + for (const auto& primitive : mesh.primitives) { + // Get the material index for this primitive + int materialIndex = primitive.material; + if (materialIndex < 0) { + materialIndex = -1; // Use -1 for primitives without materials + } + + // Initialize vectors for this material if not already done + if (materialVertices.find(materialIndex) == materialVertices.end()) { + materialVertices[materialIndex] = std::vector(); + materialIndices[materialIndex] = std::vector(); + + // Store material name for debugging + if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { + const auto& gltfMaterial = gltfModel.materials[materialIndex]; + materialNames[materialIndex] = gltfMaterial.name.empty() ? + ("material_" + std::to_string(materialIndex)) : gltfMaterial.name; + } else { + materialNames[materialIndex] = "no_material"; + } + + std::cout << " Found material " << materialIndex << ": " << materialNames[materialIndex] << " (thread-safe)" << std::endl; + } + + // Get indices for this material + if (primitive.indices >= 0) { + const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; + const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; + const tinygltf::Buffer& indexBuffer = gltfModel.buffers[indexBufferView.buffer]; + + const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; + + size_t indexOffset = materialVertices[materialIndex].size(); + + // Handle different index types + if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + const uint16_t* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + materialIndices[materialIndex].push_back(buf[i] + indexOffset); + } + } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + const uint32_t* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + materialIndices[materialIndex].push_back(buf[i] + indexOffset); + } + } + } + + // Get vertex positions + auto posIt = primitive.attributes.find("POSITION"); + if (posIt == primitive.attributes.end()) { + std::cerr << "No POSITION attribute found in primitive" << std::endl; + continue; + } + + const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; + const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; + const tinygltf::Buffer& posBuffer = gltfModel.buffers[posBufferView.buffer]; + + const float* positions = reinterpret_cast( + &posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset]); + + // Get texture coordinates (if available) + const float* texCoords = nullptr; + auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); + if (texCoordIt != primitive.attributes.end()) { + const tinygltf::Accessor& texCoordAccessor = gltfModel.accessors[texCoordIt->second]; + const tinygltf::BufferView& texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; + const tinygltf::Buffer& texCoordBuffer = gltfModel.buffers[texCoordBufferView.buffer]; + texCoords = reinterpret_cast( + &texCoordBuffer.data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]); + } + + // Get normals (if available) + const float* normals = nullptr; + auto normalIt = primitive.attributes.find("NORMAL"); + if (normalIt != primitive.attributes.end()) { + const tinygltf::Accessor& normalAccessor = gltfModel.accessors[normalIt->second]; + const tinygltf::BufferView& normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; + const tinygltf::Buffer& normalBuffer = gltfModel.buffers[normalBufferView.buffer]; + normals = reinterpret_cast( + &normalBuffer.data[normalBufferView.byteOffset + normalAccessor.byteOffset]); + } + + // Create vertices for this material + for (size_t i = 0; i < posAccessor.count; ++i) { + Vertex vertex{}; + + // Position + vertex.position = glm::vec3( + positions[i * 3 + 0], + positions[i * 3 + 1], + positions[i * 3 + 2] + ); + + // Color (use normal as color if available, otherwise white) + if (normals) { + vertex.color = glm::vec3( + std::abs(normals[i * 3 + 0]), + std::abs(normals[i * 3 + 1]), + std::abs(normals[i * 3 + 2]) + ); + } else { + vertex.color = glm::vec3(0.8f, 0.8f, 0.8f); + } + + // Texture coordinates + if (texCoords) { + vertex.texCoord = glm::vec2( + texCoords[i * 2 + 0], + texCoords[i * 2 + 1] + ); + } else { + vertex.texCoord = glm::vec2(0.0f, 0.0f); + } + + materialVertices[materialIndex].push_back(vertex); + } + } + } + + // Create material meshes with texture path information (but don't load textures) + std::vector modelMaterialMeshes; + + std::cout << "Processing " << materialVertices.size() << " materials (thread-safe):" << std::endl; + + for (const auto& materialPair : materialVertices) { + int materialIndex = materialPair.first; + const auto& vertices = materialPair.second; + const auto& indices = materialIndices[materialIndex]; + + std::cout << " Material " << materialIndex << " (" << materialNames[materialIndex] + << "): " << vertices.size() << " vertices, " << indices.size() << " indices (thread-safe)" << std::endl; + + // Create MaterialMesh for this material + MaterialMesh materialMesh; + materialMesh.materialIndex = materialIndex; + materialMesh.materialName = materialNames[materialIndex]; + materialMesh.vertices = vertices; + materialMesh.indices = indices; + + // Get texture path for this material (but don't load the texture) + if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { + const auto& gltfMaterial = gltfModel.materials[materialIndex]; + + // Try to get base color texture first + if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { + int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; + materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + } + // Fall back to other texture types if no base color + else if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { + int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; + materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + } + else if (gltfMaterial.normalTexture.index >= 0) { + int texIndex = gltfMaterial.normalTexture.index; + materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + } + } + + std::cout << " Texture path: " << (materialMesh.texturePath.empty() ? "none" : materialMesh.texturePath) << " (thread-safe)" << std::endl; + + modelMaterialMeshes.push_back(materialMesh); + } + + std::cout << "Thread-safe GLTF parsing completed with " << modelMaterialMeshes.size() << " material meshes" << std::endl; + return modelMaterialMeshes; +} diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index 498805df..d3b833e9 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -5,11 +5,74 @@ #include #include #include +#include "mesh_component.h" class Renderer; class Mesh; class Material; -class Model; + +// Forward declaration for tinygltf +namespace tinygltf { + class Model; +} + +/** + * @brief Structure representing a light source extracted from GLTF. + */ +struct ExtractedLight { + enum class Type { + Directional, + Point, + Spot, + Emissive // Light derived from emissive material + }; + + Type type = Type::Point; + glm::vec3 position = glm::vec3(0.0f); + glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // For directional/spot lights + glm::vec3 color = glm::vec3(1.0f); + float intensity = 1.0f; + float range = 100.0f; // For point/spot lights + float innerConeAngle = 0.0f; // For spot lights + float outerConeAngle = 0.785398f; // For spot lights (45 degrees) + std::string sourceMaterial; // Name of source material (for emissive lights) +}; + +/** + * @brief Structure representing mesh data for a specific material. + */ +struct MaterialMesh { + int materialIndex; + std::string materialName; + std::vector vertices; + std::vector indices; + std::string texturePath; // Primary texture path for this material +}; + +/** + * @brief Class representing a 3D model. + */ +class Model { +public: + Model(const std::string& name) : name(name) {} + ~Model() = default; + + const std::string& GetName() const { return name; } + + // Mesh data access methods + const std::vector& GetVertices() const { return vertices; } + const std::vector& GetIndices() const { return indices; } + + // Methods to set mesh data (used by parser) + void SetVertices(const std::vector& newVertices) { vertices = newVertices; } + void SetIndices(const std::vector& newIndices) { indices = newIndices; } + +private: + std::string name; + std::vector vertices; + std::vector indices; + // Other model data (meshes, materials, etc.) +}; /** * @brief Class for loading and managing 3D models. @@ -81,6 +144,34 @@ class ModelLoader { float ao, const glm::vec3& emissive); + /** + * @brief Get the first available material texture path for a model. + * @param modelName The name of the model. + * @return The texture path of the first material, or empty string if none found. + */ + std::string GetFirstMaterialTexturePath(const std::string& modelName); + + /** + * @brief Get extracted lights from a loaded model. + * @param modelName The name of the model. + * @return Vector of extracted lights from the model. + */ + std::vector GetExtractedLights(const std::string& modelName) const; + + /** + * @brief Get material-specific meshes from a loaded model. + * @param modelName The name of the model. + * @return Vector of material meshes from the model. + */ + std::vector GetMaterialMeshes(const std::string& modelName) const; + + /** + * @brief Parse GLTF file data without creating Vulkan resources (thread-safe). + * @param filename The path to the GLTF file. + * @return Vector of material meshes with raw data only. + */ + std::vector ParseGLTFDataOnly(const std::string& filename); + private: // Reference to the renderer Renderer* renderer = nullptr; @@ -91,6 +182,12 @@ class ModelLoader { // Loaded materials std::unordered_map> materials; + // Extracted lights per model + std::unordered_map> extractedLights; + + // Material meshes per model + std::unordered_map> materialMeshes; + /** * @brief Parse a GLTF file. * @param filename The path to the GLTF file. @@ -115,4 +212,20 @@ class ModelLoader { const std::string& metallicRoughnessMap, const std::string& aoMap, const std::string& emissiveMap); + + /** + * @brief Extract lights from GLTF punctual lights extension. + * @param gltfModel The loaded GLTF model. + * @param modelName The name of the model. + * @return True if extraction was successful, false otherwise. + */ + bool ExtractPunctualLights(const class tinygltf::Model& gltfModel, const std::string& modelName); + + /** + * @brief Extract lights from emissive materials. + * @param gltfModel The loaded GLTF model. + * @param modelName The name of the model. + * @return True if extraction was successful, false otherwise. + */ + bool ExtractEmissiveLights(const class tinygltf::Model& gltfModel, const std::string& modelName); }; diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index 031a7df9..c9b33e35 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -965,8 +965,13 @@ void PhysicsSystem::UpdateGPUPhysicsData() { return; } - // TODO: Add validity checks for Vulkan resources if needed - // Temporarily removed to focus on main validation error investigation + // Validate Vulkan resources before using them + if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || + *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.counterBufferMemory == VK_NULL_HANDLE || + *vulkanResources.paramsBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBufferMemory == VK_NULL_HANDLE) { + std::cerr << "PhysicsSystem::UpdateGPUPhysicsData: Invalid Vulkan resources" << std::endl; + return; + } const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); @@ -1030,8 +1035,11 @@ void PhysicsSystem::ReadbackGPUPhysicsData() { return; } - // TODO: Add validity checks for Vulkan resources if needed - // Temporarily removed to focus on main validation error investigation + // Validate Vulkan resources before using them + if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE) { + std::cerr << "PhysicsSystem::ReadbackGPUPhysicsData: Invalid Vulkan resources" << std::endl; + return; + } const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); @@ -1064,8 +1072,14 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { return; } - // TODO: Add validity checks for Vulkan resources if needed - // Temporarily removed to focus on main validation error investigation + // Validate Vulkan resources before using them + if (*vulkanResources.broadPhasePipeline == VK_NULL_HANDLE || *vulkanResources.narrowPhasePipeline == VK_NULL_HANDLE || + *vulkanResources.integratePipeline == VK_NULL_HANDLE || *vulkanResources.pipelineLayout == VK_NULL_HANDLE || + vulkanResources.descriptorSets.empty() || *vulkanResources.physicsBuffer == VK_NULL_HANDLE || + *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBuffer == VK_NULL_HANDLE) { + std::cerr << "PhysicsSystem::SimulatePhysicsOnGPU: Invalid Vulkan resources" << std::endl; + return; + } const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index ffc428d4..e8cb8c98 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -5,11 +5,7 @@ #include #include #include -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include class Entity; diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h index 20691cef..00047dec 100644 --- a/attachments/simple_engine/pipeline.h +++ b/attachments/simple_engine/pipeline.h @@ -1,10 +1,6 @@ #pragma once -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include #include #define GLM_FORCE_RADIANS diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 251067fb..c0c18bb4 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -1,10 +1,6 @@ #pragma once -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include #include #include @@ -50,9 +46,13 @@ struct UniformBufferObject { alignas(16) glm::mat4 model; alignas(16) glm::mat4 view; alignas(16) glm::mat4 proj; - alignas(16) glm::vec4 lightPos; - alignas(16) glm::vec4 lightColor; - alignas(16) glm::vec4 viewPos; + alignas(16) glm::vec4 lightPositions[4]; + alignas(16) glm::vec4 lightColors[4]; + alignas(16) glm::vec4 camPos; + alignas(4) float exposure; + alignas(4) float gamma; + alignas(4) float prefilteredCubeMipLevels; + alignas(4) float scaleIBLAmbient; }; /** @@ -208,6 +208,25 @@ class Renderer { return createShaderModule(code); } + /** + * @brief Load a texture from file. + * @param texturePath The path to the texture file. + * @return True if the texture was loaded successfully, false otherwise. + */ + bool LoadTexture(const std::string& texturePath); + + /** + * @brief Load a texture from raw image data in memory. + * @param textureId The identifier for the texture. + * @param imageData The raw image data. + * @param width The width of the image. + * @param height The height of the image. + * @param channels The number of channels in the image. + * @return True if the texture was loaded successfully, false otherwise. + */ + bool LoadTextureFromMemory(const std::string& textureId, const unsigned char* imageData, + int width, int height, int channels); + /** * @brief Transition an image layout. * @param image The image. @@ -253,12 +272,23 @@ class Renderer { void SetFramebufferResized() { framebufferResized = true; } + + /** + * @brief Set the model loader reference for accessing extracted lights. + * @param modelLoader Pointer to the model loader. + */ + void SetModelLoader(class ModelLoader* modelLoader) { + this->modelLoader = modelLoader; + } vk::Format findDepthFormat(); private: // Platform Platform* platform = nullptr; + // Model loader reference for accessing extracted lights + class ModelLoader* modelLoader = nullptr; + // Vulkan RAII context vk::raii::Context context; @@ -359,7 +389,8 @@ class Renderer { std::vector uniformBuffers; std::vector uniformBuffersMemory; std::vector uniformBuffersMapped; - std::vector descriptorSets; + std::vector basicDescriptorSets; // For basic pipeline + std::vector pbrDescriptorSets; // For PBR pipeline }; std::unordered_map entityResources; @@ -429,7 +460,7 @@ class Renderer { bool createMeshResources(MeshComponent* meshComponent); bool createUniformBuffers(Entity* entity); bool createDescriptorPool(); - bool createDescriptorSets(Entity* entity, const std::string& texturePath); + bool createDescriptorSets(Entity* entity, const std::string& texturePath, bool usePBR = false); bool createCommandBuffers(); bool createSyncObjects(); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index f1e5acfc..62f63f29 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -7,11 +7,7 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include // Debug callback for vk::raii @@ -189,7 +185,8 @@ void Renderer::Cleanup() { for (auto& memory : resources.uniformBuffersMemory) { memory.unmapMemory(); } - resources.descriptorSets.clear(); + resources.basicDescriptorSets.clear(); + resources.pbrDescriptorSets.clear(); resources.uniformBuffers.clear(); resources.uniformBuffersMemory.clear(); } diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index db0e705e..9e2e5498 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -1,6 +1,7 @@ #include "renderer.h" #include "imgui_system.h" #include "imgui/imgui.h" +#include "model_loader.h" #include #include #include @@ -270,6 +271,15 @@ void Renderer::cleanupSwapChain() { // Recreate swap chain void Renderer::recreateSwapChain() { + // Wait for all frames in flight to complete before recreating the swap chain + std::vector allFences; + for (const auto& fence : inFlightFences) { + allFences.push_back(*fence); + } + if (!allFences.empty()) { + device.waitForFences(allFences, VK_TRUE, UINT64_MAX); + } + // Wait for the device to be idle before recreating the swap chain device.waitIdle(); @@ -286,6 +296,18 @@ void Renderer::recreateSwapChain() { // Recreate descriptor pool and pipelines createDescriptorPool(); + + // Wait for all command buffers to complete before clearing resources + for (const auto& fence : inFlightFences) { + device.waitForFences(*fence, VK_TRUE, UINT64_MAX); + } + + // Clear all entity descriptor sets since they're now invalid (allocated from old pool) + for (auto& [entity, resources] : entityResources) { + resources.basicDescriptorSets.clear(); + resources.pbrDescriptorSets.clear(); + } + createGraphicsPipeline(); createPBRPipeline(); createLightingPipeline(); @@ -312,10 +334,54 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera ubo.proj = camera->GetProjectionMatrix(); ubo.proj[1][1] *= -1; // Flip Y for Vulkan - // Set light position and color - ubo.lightPos = glm::vec4(5.0f, 5.0f, 5.0f, 1.0f); - ubo.lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); - ubo.viewPos = glm::vec4(camera->GetPosition(), 1.0f); + // Set up lighting from extracted GLTF lights or fallback to default + std::vector extractedLights; + if (modelLoader) { + extractedLights = modelLoader->GetExtractedLights("../Assets/Bistro.glb"); + } + + if (!extractedLights.empty()) { + // Fill up to 4 lights from extracted lights + for (size_t i = 0; i < 4 && i < extractedLights.size(); ++i) { + const auto& light = extractedLights[i]; + ubo.lightPositions[i] = glm::vec4(light.position, 1.0f); + ubo.lightColors[i] = glm::vec4(light.color * light.intensity, 1.0f); + } + + // Fill remaining slots with dim lights if we have fewer than 4 + for (size_t i = extractedLights.size(); i < 4; ++i) { + ubo.lightPositions[i] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); + ubo.lightColors[i] = glm::vec4(0.1f, 0.1f, 0.1f, 1.0f); // Very dim fallback + } + } else { + // Fallback to default hardcoded lights if no extracted lights available + std::cout << "No extracted lights found, using fallback lighting" << std::endl; + + // Light 0: Main key light (white, positioned above and to the right) + ubo.lightPositions[0] = glm::vec4(10.0f, 10.0f, 10.0f, 1.0f); + ubo.lightColors[0] = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); + + // Light 1: Fill light (warm, positioned to the left) + ubo.lightPositions[1] = glm::vec4(-8.0f, 5.0f, 8.0f, 1.0f); + ubo.lightColors[1] = glm::vec4(0.8f, 0.7f, 0.6f, 1.0f); + + // Light 2: Rim light (cool, positioned behind) + ubo.lightPositions[2] = glm::vec4(0.0f, 8.0f, -12.0f, 1.0f); + ubo.lightColors[2] = glm::vec4(0.6f, 0.7f, 1.0f, 1.0f); + + // Light 3: Ambient fill (dim, positioned below) + ubo.lightPositions[3] = glm::vec4(0.0f, -5.0f, 5.0f, 1.0f); + ubo.lightColors[3] = glm::vec4(0.3f, 0.3f, 0.4f, 1.0f); + } + + // Set camera position for PBR calculations + ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); + + // Set PBR parameters + ubo.exposure = 1.0f; + ubo.gamma = 2.2f; + ubo.prefilteredCubeMipLevels = 0.0f; + ubo.scaleIBLAmbient = 1.0f; // Copy to uniform buffer std::memcpy(entityIt->second.uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); @@ -395,9 +461,6 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Begin dynamic rendering with vk::raii commandBuffers[currentFrame].beginRendering(renderingInfo); - // Bind the graphics pipeline - commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); - // Set the viewport vk::Viewport viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), @@ -409,6 +472,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam vk::Rect2D scissor(vk::Offset2D(0, 0), swapChainExtent); commandBuffers[currentFrame].setScissor(0, scissor); + // Track current pipeline to avoid unnecessary bindings + vk::raii::Pipeline* currentPipeline = nullptr; + vk::raii::PipelineLayout* currentLayout = nullptr; + // Render each entity for (auto entity : entities) { // Get the mesh component @@ -423,8 +490,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam continue; } - // Update the uniform buffer - updateUniformBuffer(currentFrame, entity, camera); + // Determine which pipeline to use based on ImGui PBR toggle and entity type + bool usePBR = imguiSystem && imguiSystem->IsPBREnabled() && (entity->GetName().find("Bistro") == 0); + vk::raii::Pipeline* selectedPipeline = usePBR ? &pbrGraphicsPipeline : &graphicsPipeline; + vk::raii::PipelineLayout* selectedLayout = usePBR ? &pbrPipelineLayout : &pipelineLayout; // Get the mesh resources auto meshIt = meshResources.find(meshComponent); @@ -444,14 +513,37 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam continue; } - // Create descriptor sets - if (!createDescriptorSets(entity, meshComponent->GetTexturePath())) { + // Create descriptor sets with correct pipeline type + if (!createDescriptorSets(entity, meshComponent->GetTexturePath(), usePBR)) { continue; } entityIt = entityResources.find(entity); + } else { + // Entity exists, but check if we need to create descriptor sets for the current pipeline type + auto& targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; + if (targetDescriptorSets.empty()) { + // Create missing descriptor sets for the current pipeline type + if (!createDescriptorSets(entity, meshComponent->GetTexturePath(), usePBR)) { + continue; + } + } } + // Bind pipeline if it changed + if (currentPipeline != selectedPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + + if (usePBR) { + std::cout << "Using PBR pipeline for entity: " << entity->GetName() << std::endl; + } + } + + // Update the uniform buffer + updateUniformBuffer(currentFrame, entity, camera); + // Bind the vertex buffer std::array vertexBuffers = {*meshIt->second.vertexBuffer}; std::array offsets = {0}; @@ -460,8 +552,62 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Bind the index buffer commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); - // Bind the descriptor set (dereference RAII wrapper) - commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {*entityIt->second.descriptorSets[currentFrame]}, {}); + // Bind the descriptor set using the appropriate pipeline layout + auto& selectedDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; + + // Check if descriptor sets exist for the current pipeline type + if (selectedDescriptorSets.empty()) { + std::cerr << "Error: No descriptor sets available for entity " << entity->GetName() + << " (pipeline: " << (usePBR ? "PBR" : "basic") << ")" << std::endl; + continue; // Skip this entity + } + + if (currentFrame >= selectedDescriptorSets.size()) { + std::cerr << "Error: Invalid frame index " << currentFrame + << " for entity " << entity->GetName() + << " (descriptor sets size: " << selectedDescriptorSets.size() << ")" << std::endl; + continue; // Skip this entity + } + + commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {}); + + // Push constants for PBR pipeline + if (usePBR) { + // Define PBR push constants structure matching the shader + struct PushConstants { + glm::vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; + }; + + // Set default PBR material properties + PushConstants pushConstants{}; + pushConstants.baseColorFactor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); // White base color + pushConstants.metallicFactor = 0.0f; // Non-metallic + pushConstants.roughnessFactor = 1.0f; // Fully rough + pushConstants.baseColorTextureSet = 0; // Use texture set 0 + pushConstants.physicalDescriptorTextureSet = 0; + pushConstants.normalTextureSet = 0; + pushConstants.occlusionTextureSet = 0; + pushConstants.emissiveTextureSet = 0; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + + // Push constants to the shader + commandBuffers[currentFrame].pushConstants( + **currentLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + vk::ArrayProxy(sizeof(PushConstants), reinterpret_cast(&pushConstants)) + ); + } // Draw the mesh commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, 1, 0, 0, 0); diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index f5f7a8b2..3a9d15af 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -262,6 +262,165 @@ bool Renderer::createTextureSampler(TextureResources& resources) { } } +// Load texture from file (public wrapper for createTextureImage) +bool Renderer::LoadTexture(const std::string& texturePath) { + if (texturePath.empty()) { + std::cerr << "LoadTexture: Empty texture path provided" << std::endl; + return false; + } + + // Check if texture is already loaded + auto it = textureResources.find(texturePath); + if (it != textureResources.end()) { + // Texture already loaded + return true; + } + + // Create temporary texture resources + TextureResources tempResources; + + // Use existing createTextureImage method + bool success = createTextureImage(texturePath, tempResources); + + if (success) { + std::cout << "Successfully loaded texture: " << texturePath << std::endl; + } else { + std::cerr << "Failed to load texture: " << texturePath << std::endl; + } + + return success; +} + +// Load texture from raw image data in memory +bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigned char* imageData, + int width, int height, int channels) { + if (textureId.empty() || !imageData || width <= 0 || height <= 0 || channels <= 0) { + std::cerr << "LoadTextureFromMemory: Invalid parameters" << std::endl; + return false; + } + + // Check if texture is already loaded + auto it = textureResources.find(textureId); + if (it != textureResources.end()) { + // Texture already loaded + return true; + } + + try { + TextureResources resources; + + // Calculate image size (ensure 4 channels for RGBA) + int targetChannels = 4; // Always use RGBA for consistency + vk::DeviceSize imageSize = width * height * targetChannels; + + // Create staging buffer + auto [stagingBuffer, stagingBufferMemory] = createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy and convert pixel data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, imageSize); + unsigned char* stagingData = static_cast(data); + + if (channels == 4) { + // Already RGBA, direct copy + memcpy(stagingData, imageData, imageSize); + } else if (channels == 3) { + // RGB to RGBA conversion + for (int i = 0; i < width * height; ++i) { + stagingData[i * 4 + 0] = imageData[i * 3 + 0]; // R + stagingData[i * 4 + 1] = imageData[i * 3 + 1]; // G + stagingData[i * 4 + 2] = imageData[i * 3 + 2]; // B + stagingData[i * 4 + 3] = 255; // A + } + } else if (channels == 2) { + // Grayscale + Alpha to RGBA conversion + for (int i = 0; i < width * height; ++i) { + stagingData[i * 4 + 0] = imageData[i * 2 + 0]; // R (grayscale) + stagingData[i * 4 + 1] = imageData[i * 2 + 0]; // G (grayscale) + stagingData[i * 4 + 2] = imageData[i * 2 + 0]; // B (grayscale) + stagingData[i * 4 + 3] = imageData[i * 2 + 1]; // A (alpha) + } + } else if (channels == 1) { + // Grayscale to RGBA conversion + for (int i = 0; i < width * height; ++i) { + stagingData[i * 4 + 0] = imageData[i]; // R + stagingData[i * 4 + 1] = imageData[i]; // G + stagingData[i * 4 + 2] = imageData[i]; // B + stagingData[i * 4 + 3] = 255; // A + } + } else { + std::cerr << "LoadTextureFromMemory: Unsupported channel count: " << channels << std::endl; + stagingBufferMemory.unmapMemory(); + return false; + } + + stagingBufferMemory.unmapMemory(); + + // Create texture image + auto [textureImg, textureImgMem] = createImage( + width, + height, + vk::Format::eR8G8B8A8Srgb, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + resources.textureImage = std::move(textureImg); + resources.textureImageMemory = std::move(textureImgMem); + + // Transition image layout for copy + transitionImageLayout( + *resources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal + ); + + // Copy buffer to image + copyBufferToImage( + *stagingBuffer, + *resources.textureImage, + static_cast(width), + static_cast(height) + ); + + // Transition image layout for shader access + transitionImageLayout( + *resources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eShaderReadOnlyOptimal + ); + + // Create texture image view + resources.textureImageView = createImageView( + resources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageAspectFlagBits::eColor + ); + + // Create texture sampler + if (!createTextureSampler(resources)) { + return false; + } + + // Add to texture resources map + textureResources[textureId] = std::move(resources); + + std::cout << "Successfully loaded texture from memory: " << textureId + << " (" << width << "x" << height << ", " << channels << " channels)" << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Failed to load texture from memory: " << e.what() << std::endl; + return false; + } +} + // Create mesh resources bool Renderer::createMeshResources(MeshComponent* meshComponent) { try { @@ -385,26 +544,44 @@ bool Renderer::createUniformBuffers(Entity* entity) { // Create descriptor pool bool Renderer::createDescriptorPool() { try { - // Create descriptor pool sizes + // Calculate pool sizes for all Bistro materials (131) plus additional entities + // Each entity needs descriptor sets for both basic and PBR pipelines + // PBR pipeline needs 6 descriptors per set (1 UBO + 5 textures) + // Basic pipeline needs 2 descriptors per set (1 UBO + 1 texture) + const uint32_t maxEntities = 200; // Support up to 200 entities (131 Bistro + extras) + const uint32_t maxDescriptorSets = MAX_FRAMES_IN_FLIGHT * maxEntities * 2; // 2 pipeline types per entity + + // Calculate descriptor counts + // UBO descriptors: 1 per descriptor set + const uint32_t uboDescriptors = maxDescriptorSets; + // Texture descriptors: Basic pipeline uses 1, PBR uses 5, so average ~3 per set + // But allocate for worst case: all entities using PBR (5 textures each) + const uint32_t textureDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities * 5; + std::array poolSizes = { vk::DescriptorPoolSize{ .type = vk::DescriptorType::eUniformBuffer, - .descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT * 100) + .descriptorCount = uboDescriptors }, vk::DescriptorPoolSize{ .type = vk::DescriptorType::eCombinedImageSampler, - .descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT * 100) + .descriptorCount = textureDescriptors } }; // Create descriptor pool vk::DescriptorPoolCreateInfo poolInfo{ .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .maxSets = static_cast(MAX_FRAMES_IN_FLIGHT * 100), + .maxSets = maxDescriptorSets, .poolSizeCount = static_cast(poolSizes.size()), .pPoolSizes = poolSizes.data() }; + std::cout << "Creating descriptor pool with capacity for " << maxEntities << " entities:" << std::endl; + std::cout << " Max descriptor sets: " << maxDescriptorSets << std::endl; + std::cout << " UBO descriptors: " << uboDescriptors << std::endl; + std::cout << " Texture descriptors: " << textureDescriptors << std::endl; + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); return true; } catch (const std::exception& e) { @@ -414,7 +591,7 @@ bool Renderer::createDescriptorPool() { } // Create descriptor sets -bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePath) { +bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePath, bool usePBR) { try { // Get entity resources auto entityIt = entityResources.find(entity); @@ -423,37 +600,50 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa return false; } - // Get texture resources - TextureResources textureRes; + // Get texture resources - don't move them out of the map! + TextureResources* textureRes = nullptr; if (!texturePath.empty()) { auto textureIt = textureResources.find(texturePath); if (textureIt == textureResources.end()) { // Create texture resources if they don't exist - if (!createTextureImage(texturePath, textureRes)) { + TextureResources tempRes; + if (!createTextureImage(texturePath, tempRes)) { + return false; + } + // The texture should now be in the map, find it again + textureIt = textureResources.find(texturePath); + if (textureIt == textureResources.end()) { + std::cerr << "Failed to find texture after creation: " << texturePath << std::endl; return false; } - } else { - textureRes = std::move(textureIt->second); } + textureRes = &textureIt->second; } - // Create descriptor sets using RAII - std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); + // Create descriptor sets using RAII - choose layout based on pipeline type + vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; + std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), .pSetLayouts = layouts.data() }; - // Allocate descriptor sets using RAII wrapper - vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); + // Choose the appropriate descriptor set vector based on pipeline type + auto& targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; - // Convert to vector of individual RAII descriptor sets - entityIt->second.descriptorSets.clear(); - entityIt->second.descriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - entityIt->second.descriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); - std::cout << "Created descriptor set " << i << " with handle: " << *entityIt->second.descriptorSets[i] << std::endl; + // Only create descriptor sets if they don't already exist for this pipeline type + if (targetDescriptorSets.empty()) { + // Allocate descriptor sets using RAII wrapper + vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); + + // Convert to vector of individual RAII descriptor sets + targetDescriptorSets.clear(); + targetDescriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + targetDescriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); + std::cout << "Created " << (usePBR ? "PBR" : "basic") << " descriptor set " << i << " with handle: " << *targetDescriptorSets[i] << std::endl; + } } // Update descriptor sets @@ -465,54 +655,113 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .range = sizeof(UniformBufferObject) }; - // Always update both descriptors - std::array descriptorWrites; - - // Uniform buffer descriptor write - descriptorWrites[0] = vk::WriteDescriptorSet{ - .dstSet = entityIt->second.descriptorSets[i], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pBufferInfo = &bufferInfo - }; - - // Check if texture resources are valid - bool hasValidTexture = !texturePath.empty() && - *textureRes.textureSampler && - *textureRes.textureImageView; - - // Texture sampler descriptor - vk::DescriptorImageInfo imageInfo; - if (hasValidTexture) { - // Use provided texture resources - imageInfo = vk::DescriptorImageInfo{ - .sampler = *textureRes.textureSampler, - .imageView = *textureRes.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + if (usePBR) { + // PBR pipeline: Create 6 descriptor writes (UBO + 5 textures) + std::array descriptorWrites; + std::array imageInfos; + + // Uniform buffer descriptor write (binding 0) + descriptorWrites[0] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo }; - } else { - // Use default texture resources - imageInfo = vk::DescriptorImageInfo{ + + // Try to use loaded textures instead of default white texture + vk::DescriptorImageInfo defaultImageInfo{ .sampler = *defaultTextureResources.textureSampler, .imageView = *defaultTextureResources.textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal }; - } - // Texture sampler descriptor write - descriptorWrites[1] = vk::WriteDescriptorSet{ - .dstSet = entityIt->second.descriptorSets[i], - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = &imageInfo - }; + // Use entity-specific texture if available, otherwise fall back to default + bool hasValidTexture = !texturePath.empty() && + textureResources.find(texturePath) != textureResources.end(); + + for (int j = 0; j < 5; j++) { + if (hasValidTexture) { + // Use the entity's specific texture for all PBR bindings + const auto& texRes = textureResources.at(texturePath); + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *texRes.textureSampler, + .imageView = *texRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + std::cout << "Using entity texture for PBR binding " << (j+1) << ": " << texturePath << std::endl; + } else { + // Fall back to default texture + imageInfos[j] = defaultImageInfo; + std::cout << "Using default texture for PBR binding " << (j+1) << std::endl; + } + } + + // Create descriptor writes for all 5 texture bindings + for (int binding = 1; binding <= 5; binding++) { + descriptorWrites[binding] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = static_cast(binding), + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfos[binding - 1] + }; + } - // Update descriptor sets with both descriptors - device.updateDescriptorSets(descriptorWrites, {}); + // Update descriptor sets with all 6 descriptors + device.updateDescriptorSets(descriptorWrites, {}); + } else { + // Basic pipeline: Create 2 descriptor writes (UBO + 1 texture) + std::array descriptorWrites; + + // Uniform buffer descriptor write + descriptorWrites[0] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }; + + // Check if texture resources are valid + bool hasValidTexture = !texturePath.empty() && textureRes && + *textureRes->textureSampler && + *textureRes->textureImageView; + + // Texture sampler descriptor + vk::DescriptorImageInfo imageInfo; + if (hasValidTexture) { + // Use provided texture resources + imageInfo = vk::DescriptorImageInfo{ + .sampler = *textureRes->textureSampler, + .imageView = *textureRes->textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + // Use default texture resources + imageInfo = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + + // Texture sampler descriptor write + descriptorWrites[1] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + // Update descriptor sets with both descriptors + device.updateDescriptorSets(descriptorWrites, {}); + } } return true; diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp new file mode 100644 index 00000000..c2a8e5a7 --- /dev/null +++ b/attachments/simple_engine/scene_loading.cpp @@ -0,0 +1,122 @@ +#include "scene_loading.h" +#include "engine.h" +#include "transform_component.h" +#include "mesh_component.h" +#include +#include + +// Global loading state definition +LoadingState g_loadingState; + +/** + * @brief Background thread function to load the Bistro model. + * @param modelLoader Pointer to the model loader. + */ +void LoadBistroModelAsync(ModelLoader* modelLoader) { + try { + std::cout << "Starting thread-safe background loading of Bistro model..." << std::endl; + g_loadingState.isLoading = true; + g_loadingState.loadingComplete = false; + g_loadingState.loadingFailed = false; + + // Parse GLTF data without creating Vulkan resources (thread-safe) + std::vector materialMeshes = modelLoader->ParseGLTFDataOnly("../Assets/Bistro.glb"); + if (materialMeshes.empty()) { + g_loadingState.errorMessage = "Failed to parse Bistro.glb - no material meshes found"; + g_loadingState.loadingFailed = true; + g_loadingState.isLoading = false; + return; + } + + g_loadingState.totalMaterials = static_cast(materialMeshes.size()); + + std::cout << "Parsed " << materialMeshes.size() << " materials in background thread (thread-safe)" << std::endl; + + // Store the loaded materials (thread-safe copy) + { + std::lock_guard lock(g_loadingState.entityCreationMutex); + g_loadingState.loadedMaterials = std::move(materialMeshes); + } + + g_loadingState.loadingComplete = true; + g_loadingState.isLoading = false; + std::cout << "Thread-safe background loading completed successfully!" << std::endl; + + } catch (const std::exception& e) { + g_loadingState.errorMessage = std::string("Thread-safe loading error: ") + e.what(); + g_loadingState.loadingFailed = true; + g_loadingState.isLoading = false; + std::cerr << "Thread-safe background loading failed: " << e.what() << std::endl; + } +} + +/** + * @brief Create entities from loaded materials (called from main thread). + * @param engine The engine to create entities in. + */ +void CreateEntitiesFromLoadedMaterials(Engine* engine) { + std::lock_guard lock(g_loadingState.entityCreationMutex); + + if (g_loadingState.loadedMaterials.empty()) { + return; + } + + std::cout << "Creating " << g_loadingState.loadedMaterials.size() << " entities from loaded materials..." << std::endl; + + // Get the model loader and renderer for texture loading on main thread + ModelLoader* modelLoader = engine->GetModelLoader(); + Renderer* renderer = engine->GetRenderer(); + + // First, load the actual GLTF model with Vulkan resources on the main thread + // This will create all the textures that the background thread couldn't create + if (modelLoader && renderer) { + std::cout << "Loading Bistro model with Vulkan resources on main thread..." << std::endl; + Model* bistroModel = modelLoader->LoadGLTF("../Assets/Bistro.glb"); + if (bistroModel) { + std::cout << "Successfully loaded Bistro model with Vulkan resources on main thread" << std::endl; + } else { + std::cerr << "Warning: Failed to load Bistro model with Vulkan resources on main thread" << std::endl; + } + } + + int entitiesCreated = 0; + for (const auto& materialMesh : g_loadingState.loadedMaterials) { + // Create entity name based on material + std::string entityName = "Bistro_Material_" + std::to_string(materialMesh.materialIndex) + + "_" + materialMesh.materialName; + + if (Entity* materialEntity = engine->CreateEntity(entityName)) { + // Add transform component + auto* transform = materialEntity->AddComponent(); + transform->SetPosition(glm::vec3(0.0f, -1.5f, 0.0f)); + transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); + transform->SetScale(glm::vec3(0.1f, 0.1f, 0.1f)); + + // Add mesh component with material-specific data + auto* mesh = materialEntity->AddComponent(); + mesh->SetVertices(materialMesh.vertices); + mesh->SetIndices(materialMesh.indices); + + // Set the correct texture for this material + // The texture should now be loaded since we called LoadGLTF on the main thread + if (!materialMesh.texturePath.empty()) { + mesh->SetTexturePath(materialMesh.texturePath); + std::cout << " Entity " << entityName << ": " << materialMesh.vertices.size() + << " vertices, texture: " << materialMesh.texturePath << std::endl; + } else { + std::cout << " Entity " << entityName << ": " << materialMesh.vertices.size() + << " vertices, no texture" << std::endl; + } + + entitiesCreated++; + g_loadingState.materialsLoaded = entitiesCreated; + } else { + std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; + } + } + + std::cout << "Successfully created " << entitiesCreated << " entities from loaded materials" << std::endl; + + // Clear the loaded materials to free memory + g_loadingState.loadedMaterials.clear(); +} diff --git a/attachments/simple_engine/scene_loading.h b/attachments/simple_engine/scene_loading.h new file mode 100644 index 00000000..e4ec5800 --- /dev/null +++ b/attachments/simple_engine/scene_loading.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include "model_loader.h" + +// Forward declarations +class Engine; +class ModelLoader; + +// Structure to track threaded loading state +struct LoadingState { + std::atomic isLoading{false}; + std::atomic loadingComplete{false}; + std::atomic loadingFailed{false}; + std::atomic materialsLoaded{0}; + std::atomic totalMaterials{0}; + std::mutex entityCreationMutex; + std::vector loadedMaterials; + std::string errorMessage; +}; + +// Global loading state +extern LoadingState g_loadingState; + +/** + * @brief Background thread function to load the Bistro model. + * @param modelLoader Pointer to the model loader. + */ +void LoadBistroModelAsync(ModelLoader* modelLoader); + +/** + * @brief Create entities from loaded materials (called from main thread). + * @param engine The engine to create entities in. + */ +void CreateEntitiesFromLoadedMaterials(Engine* engine); diff --git a/attachments/simple_engine/swap_chain.h b/attachments/simple_engine/swap_chain.h index 1b79067c..0baeda21 100644 --- a/attachments/simple_engine/swap_chain.h +++ b/attachments/simple_engine/swap_chain.h @@ -1,10 +1,6 @@ #pragma once -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif #include #include #include diff --git a/attachments/simple_engine/vulkan_device.h b/attachments/simple_engine/vulkan_device.h index ad1b63ce..8d5b55db 100644 --- a/attachments/simple_engine/vulkan_device.h +++ b/attachments/simple_engine/vulkan_device.h @@ -3,11 +3,7 @@ #include #include #include -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif /** * @brief Structure for Vulkan queue family indices. diff --git a/attachments/simple_engine/vulkan_dispatch.cpp b/attachments/simple_engine/vulkan_dispatch.cpp index e9f05099..daa4a73e 100644 --- a/attachments/simple_engine/vulkan_dispatch.cpp +++ b/attachments/simple_engine/vulkan_dispatch.cpp @@ -1,8 +1,4 @@ -#ifdef __INTELLISENSE__ #include -#else -import vulkan_hpp; -#endif // Define the defaultDispatchLoaderDynamic variable namespace vk::detail { From a62e4a9fe00fed99d300e5a1eada838eb6a9d0f8 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 25 Jul 2025 01:26:11 -0700 Subject: [PATCH 030/102] make the hrtf listener sync with the camera location and rotation. --- attachments/simple_engine/audio_system.cpp | 24 +++++++++++++++++++- attachments/simple_engine/audio_system.h | 7 +++++- attachments/simple_engine/camera_component.h | 16 +++++++++++++ attachments/simple_engine/engine.cpp | 4 ++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index ca73eb56..714a3047 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -22,6 +22,7 @@ #endif #include "renderer.h" +#include "engine.h" // OpenAL error checking utility static void CheckOpenALError(const std::string& operation) { @@ -537,7 +538,10 @@ void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint } -bool AudioSystem::Initialize(Renderer* renderer) { +bool AudioSystem::Initialize(Engine* engine, Renderer* renderer) { + // Store the engine reference for accessing active camera + this->engine = engine; + if (renderer) { // Validate renderer if provided if (!renderer->IsInitialized()) { @@ -588,6 +592,24 @@ void AudioSystem::Update(float deltaTime) { return; } + // Synchronize HRTF listener position and orientation with active camera + if (engine) { + CameraComponent* activeCamera = engine->GetActiveCamera(); + if (activeCamera) { + // Get camera position + glm::vec3 cameraPos = activeCamera->GetPosition(); + SetListenerPosition(cameraPos.x, cameraPos.y, cameraPos.z); + + // Calculate camera forward and up vectors for orientation + // The camera looks at its target, so forward = normalize(target - position) + glm::vec3 target = activeCamera->GetTarget(); + glm::vec3 up = activeCamera->GetUp(); + glm::vec3 forward = glm::normalize(target - cameraPos); + + SetListenerOrientation(forward.x, forward.y, forward.z, up.x, up.y, up.z); + } + } + // Update audio sources and process spatial audio for (auto& source : sources) { if (!source->IsPlaying()) { diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index fe22ed9a..f3193396 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -79,6 +79,7 @@ class AudioSource { // Forward declarations class Renderer; +class Engine; /** * @brief Interface for audio output devices. @@ -154,10 +155,11 @@ class AudioSystem { /** * @brief Initialize the audio system. + * @param engine Pointer to the engine for accessing active camera. * @param renderer Pointer to the renderer for compute shader support. * @return True if initialization was successful, false otherwise. */ - bool Initialize(Renderer* renderer = nullptr); + bool Initialize(Engine* engine, Renderer* renderer = nullptr); /** * @brief Update the audio system. @@ -298,6 +300,9 @@ class AudioSystem { // Renderer for compute shader support Renderer* renderer = nullptr; + // Engine reference for accessing active camera + Engine* engine = nullptr; + // Audio output device for sending processed audio to speakers std::unique_ptr outputDevice = nullptr; diff --git a/attachments/simple_engine/camera_component.h b/attachments/simple_engine/camera_component.h index 6c6a33c1..82d0b25c 100644 --- a/attachments/simple_engine/camera_component.h +++ b/attachments/simple_engine/camera_component.h @@ -182,6 +182,22 @@ class CameraComponent : public Component { return transform ? transform->GetPosition() : glm::vec3(0.0f, 0.0f, 0.0f); } + /** + * @brief Get the camera target. + * @return The camera target. + */ + const glm::vec3& GetTarget() const { + return target; + } + + /** + * @brief Get the camera up vector. + * @return The camera up vector. + */ + const glm::vec3& GetUp() const { + return up; + } + private: /** * @brief Update the view matrix based on the camera position and target. diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index df605ab9..2c13e363 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -133,7 +133,7 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool renderer->SetModelLoader(modelLoader.get()); // Initialize audio system - if (!audioSystem->Initialize(renderer.get())) { + if (!audioSystem->Initialize(this, renderer.get())) { return false; } @@ -516,7 +516,7 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo renderer->SetModelLoader(modelLoader.get()); // Initialize audio system - if (!audioSystem->Initialize(renderer.get())) { + if (!audioSystem->Initialize(this, renderer.get())) { return false; } From 14fc6b1fdd97d3894ad0e05006ad74152af10888 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 25 Jul 2025 14:47:02 -0700 Subject: [PATCH 031/102] Lighting working for non PBR code, and PBR code seems to be relatively working as well. Introduce memorypool and stop doing lazy loading of objects which exhausts system of memory. Move loading into separate thread. --- attachments/simple_engine/CMakeLists.txt | 1 + attachments/simple_engine/main.cpp | 55 +-- attachments/simple_engine/memory_pool.cpp | 465 ++++++++++++++++++ attachments/simple_engine/memory_pool.h | 195 ++++++++ attachments/simple_engine/mesh_component.cpp | 72 +-- attachments/simple_engine/mesh_component.h | 20 +- attachments/simple_engine/model_loader.cpp | 223 ++++----- attachments/simple_engine/model_loader.h | 43 +- attachments/simple_engine/pipeline.cpp | 4 +- attachments/simple_engine/renderer.h | 32 +- attachments/simple_engine/renderer_core.cpp | 31 +- .../simple_engine/renderer_rendering.cpp | 152 +++--- .../simple_engine/renderer_resources.cpp | 346 ++++++++++++- attachments/simple_engine/scene_loading.cpp | 8 +- attachments/simple_engine/shaders/pbr.slang | 8 +- .../simple_engine/shaders/texturedMesh.slang | 38 +- 16 files changed, 1330 insertions(+), 363 deletions(-) create mode 100644 attachments/simple_engine/memory_pool.cpp create mode 100644 attachments/simple_engine/memory_pool.h diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index bef18179..3c519986 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -97,6 +97,7 @@ set(SOURCES renderer_compute.cpp renderer_utils.cpp renderer_resources.cpp + memory_pool.cpp resource_manager.cpp entity.cpp component.cpp diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index b07abd5d..18f246b3 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -36,57 +36,6 @@ void SetupScene(Engine* engine) { // Set the camera as the active camera engine->SetActiveCamera(camera); - // Create a cube entity - Entity* cubeEntity = engine->CreateEntity("Cube"); - if (!cubeEntity) { - throw std::runtime_error("Failed to create cube entity"); - } - - // Add a transform component to the cube - auto* cubeTransform = cubeEntity->AddComponent(); - cubeTransform->SetPosition(glm::vec3(0.0f, 0.0f, 0.0f)); - cubeTransform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); - cubeTransform->SetScale(glm::vec3(1.0f, 1.0f, 1.0f)); - - // Add a mesh component to the cube - auto* cubeMesh = cubeEntity->AddComponent(); - cubeMesh->CreateCube(1.0f, glm::vec3(1.0f, 0.0f, 0.0f)); - - // Make the camera look at the red cube - camera->LookAt(cubeTransform->GetPosition()); - - // Create a second cube entity - Entity* cube2Entity = engine->CreateEntity("Cube2"); - if (!cube2Entity) { - throw std::runtime_error("Failed to create second cube entity"); - } - - // Add a transform component to the second cube - auto* cube2Transform = cube2Entity->AddComponent(); - cube2Transform->SetPosition(glm::vec3(2.0f, 0.0f, 0.0f)); - cube2Transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); - cube2Transform->SetScale(glm::vec3(0.5f, 0.5f, 0.5f)); - - // Add a mesh component to the second cube - auto* cube2Mesh = cube2Entity->AddComponent(); - cube2Mesh->CreateCube(1.0f, glm::vec3(0.0f, 1.0f, 0.0f)); - - // Create a third cube entity - Entity* cube3Entity = engine->CreateEntity("Cube3"); - if (!cube3Entity) { - throw std::runtime_error("Failed to create third cube entity"); - } - - // Add a transform component to the third cube - auto* cube3Transform = cube3Entity->AddComponent(); - cube3Transform->SetPosition(glm::vec3(-2.0f, 0.0f, 0.0f)); - cube3Transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); - cube3Transform->SetScale(glm::vec3(0.5f, 0.5f, 0.5f)); - - // Add a mesh component to the third cube - auto* cube3Mesh = cube3Entity->AddComponent(); - cube3Mesh->CreateCube(1.0f, glm::vec3(0.0f, 0.0f, 1.0f)); - // Start loading Bistro.glb model in background thread if (ModelLoader* modelLoader = engine->GetModelLoader()) { std::cout << "Starting threaded loading of Bistro model..." << std::endl; @@ -123,11 +72,9 @@ void android_main(android_app* app) { #else /** * @brief Desktop entry point. - * @param argc The number of command-line arguments. - * @param argv The command-line arguments. * @return The exit code. */ -int main(int argc, char* argv[]) { +int main(int, char*[]) { try { // Create the engine Engine engine; diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp new file mode 100644 index 00000000..118d276d --- /dev/null +++ b/attachments/simple_engine/memory_pool.cpp @@ -0,0 +1,465 @@ +#include "memory_pool.h" +#include +#include +#include + +MemoryPool::MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalDevice& physicalDevice) + : device(device), physicalDevice(physicalDevice) { +} + +MemoryPool::~MemoryPool() { + // RAII will handle cleanup automatically + std::lock_guard lock(poolMutex); + pools.clear(); +} + +bool MemoryPool::initialize() { + std::lock_guard lock(poolMutex); + + try { + // Configure default pool settings based on typical usage patterns + + // Vertex buffer pool: Large allocations, device-local + configurePool( + PoolType::VERTEX_BUFFER, + 64 * 1024 * 1024, // 64MB blocks + 4096, // 4KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal, + 8 // Max 8 blocks (512MB total) + ); + + // Index buffer pool: Medium allocations, device-local + configurePool( + PoolType::INDEX_BUFFER, + 32 * 1024 * 1024, // 32MB blocks + 2048, // 2KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal, + 4 // Max 4 blocks (128MB total) + ); + + // Uniform buffer pool: Small allocations, host-visible + configurePool( + PoolType::UNIFORM_BUFFER, + 4 * 1024 * 1024, // 4MB blocks + 256, // 256B allocation units + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + 4 // Max 4 blocks (16MB total) + ); + + // Staging buffer pool: Variable allocations, host-visible + configurePool( + PoolType::STAGING_BUFFER, + 16 * 1024 * 1024, // 16MB blocks + 1024, // 1KB allocation units + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + 4 // Max 4 blocks (64MB total) + ); + + // Texture image pool: Large allocations, device-local + configurePool( + PoolType::TEXTURE_IMAGE, + 128 * 1024 * 1024, // 128MB blocks + 4096, // 4KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal, + 8 // Max 8 blocks (1GB total) + ); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to initialize memory pool: " << e.what() << std::endl; + return false; + } +} + +void MemoryPool::configurePool( + PoolType poolType, + vk::DeviceSize blockSize, + vk::DeviceSize allocationUnit, + vk::MemoryPropertyFlags properties, + uint32_t maxBlocks) { + + PoolConfig config; + config.blockSize = blockSize; + config.allocationUnit = allocationUnit; + config.properties = properties; + config.maxBlocks = maxBlocks; + + poolConfigs[poolType] = config; +} + +uint32_t MemoryPool::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && + (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); +} + +std::unique_ptr MemoryPool::createMemoryBlock(PoolType poolType, vk::DeviceSize size) { + auto configIt = poolConfigs.find(poolType); + if (configIt == poolConfigs.end()) { + throw std::runtime_error("Pool type not configured"); + } + + const PoolConfig& config = configIt->second; + + // Use the larger of requested size or configured block size + vk::DeviceSize blockSize = std::max(size, config.blockSize); + + // Create a dummy buffer to get memory requirements for the memory type + vk::BufferCreateInfo bufferInfo{ + .size = blockSize, + .usage = vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | + vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eTransferSrc | + vk::BufferUsageFlagBits::eTransferDst, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer dummyBuffer(device, bufferInfo); + vk::MemoryRequirements memRequirements = dummyBuffer.getMemoryRequirements(); + + uint32_t memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, config.properties); + + // Allocate the memory block + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = blockSize, + .memoryTypeIndex = memoryTypeIndex + }; + + // Create MemoryBlock with proper initialization to avoid default constructor issues + auto block = std::unique_ptr(new MemoryBlock{ + .memory = vk::raii::DeviceMemory(device, allocInfo), + .size = blockSize, + .used = 0, + .memoryTypeIndex = memoryTypeIndex, + .isMapped = false, + .mappedPtr = nullptr, + .freeList = {}, + .allocationUnit = config.allocationUnit + }); + + // Map memory if it's host-visible + block->isMapped = (config.properties & vk::MemoryPropertyFlagBits::eHostVisible) != vk::MemoryPropertyFlags{}; + if (block->isMapped) { + block->mappedPtr = block->memory.mapMemory(0, blockSize); + } else { + block->mappedPtr = nullptr; + } + + // Initialize free list + size_t numUnits = static_cast(blockSize / config.allocationUnit); + block->freeList.resize(numUnits, true); // All units initially free + + + return block; +} + +MemoryPool::MemoryBlock* MemoryPool::findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + pools[poolType] = std::vector>(); + poolIt = pools.find(poolType); + } + + auto& poolBlocks = poolIt->second; + const PoolConfig& config = poolConfigs[poolType]; + + // Calculate required units (accounting for alignment) + vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); + + // Search existing blocks for sufficient free space + for (auto& block : poolBlocks) { + // Find consecutive free units + size_t consecutiveFree = 0; + for (size_t i = 0; i < block->freeList.size(); ++i) { + if (block->freeList[i]) { + consecutiveFree++; + if (consecutiveFree >= requiredUnits) { + return block.get(); + } + } else { + consecutiveFree = 0; + } + } + } + + // No suitable block found, create a new one if we haven't reached the limit AND rendering is not active + if (poolBlocks.size() < config.maxBlocks) { + if (renderingActive) { + std::cerr << "ERROR: Attempted to create new memory block during rendering! Pool type: " + << static_cast(poolType) << ", required size: " << alignedSize << std::endl; + std::cerr << "This violates the constraint that no new memory should be allocated during PBR rendering." << std::endl; + return nullptr; + } + + try { + auto newBlock = createMemoryBlock(poolType, alignedSize); + MemoryBlock* blockPtr = newBlock.get(); + poolBlocks.push_back(std::move(newBlock)); + std::cout << "Created new memory block during initialization (pool type: " + << static_cast(poolType) << ")" << std::endl; + return blockPtr; + } catch (const std::exception& e) { + std::cerr << "Failed to create new memory block: " << e.what() << std::endl; + return nullptr; + } + } + + std::cerr << "Memory pool exhausted for pool type " << static_cast(poolType) << std::endl; + return nullptr; +} + +std::unique_ptr MemoryPool::allocate(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { + std::lock_guard lock(poolMutex); + + MemoryBlock* block = findSuitableBlock(poolType, size, alignment); + if (!block) { + return nullptr; + } + + const PoolConfig& config = poolConfigs[poolType]; + + // Calculate required units (accounting for alignment) + vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); + + // Find consecutive free units + size_t startUnit = 0; + size_t consecutiveFree = 0; + bool found = false; + + for (size_t i = 0; i < block->freeList.size(); ++i) { + if (block->freeList[i]) { + if (consecutiveFree == 0) { + startUnit = i; + } + consecutiveFree++; + if (consecutiveFree >= requiredUnits) { + found = true; + break; + } + } else { + consecutiveFree = 0; + } + } + + if (!found) { + return nullptr; + } + + // Mark units as used + for (size_t i = startUnit; i < startUnit + requiredUnits; ++i) { + block->freeList[i] = false; + } + + // Create allocation info + auto allocation = std::make_unique(); + allocation->memory = *block->memory; + allocation->offset = startUnit * config.allocationUnit; + allocation->size = alignedSize; + allocation->memoryTypeIndex = block->memoryTypeIndex; + allocation->isMapped = block->isMapped; + allocation->mappedPtr = block->isMapped ? + static_cast(block->mappedPtr) + allocation->offset : nullptr; + + block->used += alignedSize; + + return allocation; +} + +void MemoryPool::deallocate(std::unique_ptr allocation) { + if (!allocation) { + return; + } + + std::lock_guard lock(poolMutex); + + // Find the block that contains this allocation + for (auto& [poolType, poolBlocks] : pools) { + const PoolConfig& config = poolConfigs[poolType]; + + for (auto& block : poolBlocks) { + if (*block->memory == allocation->memory) { + // Calculate which units to free + size_t startUnit = static_cast(allocation->offset / config.allocationUnit); + size_t numUnits = static_cast((allocation->size + config.allocationUnit - 1) / config.allocationUnit); + + // Mark units as free + for (size_t i = startUnit; i < startUnit + numUnits; ++i) { + if (i < block->freeList.size()) { + block->freeList[i] = true; + } + } + + block->used -= allocation->size; + return; + } + } + } + + std::cerr << "Warning: Could not find memory block for deallocation" << std::endl; +} + +std::pair> MemoryPool::createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + + // Determine pool type based on usage and properties + PoolType poolType; + if (usage & vk::BufferUsageFlagBits::eVertexBuffer) { + poolType = PoolType::VERTEX_BUFFER; + } else if (usage & vk::BufferUsageFlagBits::eIndexBuffer) { + poolType = PoolType::INDEX_BUFFER; + } else if (usage & vk::BufferUsageFlagBits::eUniformBuffer) { + poolType = PoolType::UNIFORM_BUFFER; + } else if (properties & vk::MemoryPropertyFlagBits::eHostVisible) { + poolType = PoolType::STAGING_BUFFER; + } else { + poolType = PoolType::VERTEX_BUFFER; // Default to vertex buffer pool + } + + // Create the buffer + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer buffer(device, bufferInfo); + + // Get memory requirements + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + // Allocate from pool + auto allocation = allocate(poolType, memRequirements.size, memRequirements.alignment); + if (!allocation) { + throw std::runtime_error("Failed to allocate memory from pool"); + } + + // Bind memory to buffer + buffer.bindMemory(allocation->memory, allocation->offset); + + return {std::move(buffer), std::move(allocation)}; +} + +std::pair> MemoryPool::createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties) { + + // Create the image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + vk::raii::Image image(device, imageInfo); + + // Get memory requirements + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + + // Allocate from texture pool + auto allocation = allocate(PoolType::TEXTURE_IMAGE, memRequirements.size, memRequirements.alignment); + if (!allocation) { + throw std::runtime_error("Failed to allocate memory from texture pool"); + } + + // Bind memory to image + image.bindMemory(allocation->memory, allocation->offset); + + return {std::move(image), std::move(allocation)}; +} + +std::pair MemoryPool::getMemoryUsage(PoolType poolType) const { + std::lock_guard lock(poolMutex); + + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + return {0, 0}; + } + + vk::DeviceSize used = 0; + vk::DeviceSize total = 0; + + for (const auto& block : poolIt->second) { + used += block->used; + total += block->size; + } + + return {used, total}; +} + +std::pair MemoryPool::getTotalMemoryUsage() const { + std::lock_guard lock(poolMutex); + + vk::DeviceSize totalUsed = 0; + vk::DeviceSize totalAllocated = 0; + + for (const auto& [poolType, poolBlocks] : pools) { + for (const auto& block : poolBlocks) { + totalUsed += block->used; + totalAllocated += block->size; + } + } + + return {totalUsed, totalAllocated}; +} + +bool MemoryPool::preAllocatePools() { + std::lock_guard lock(poolMutex); + + try { + std::cout << "Pre-allocating memory pools to prevent allocation during rendering..." << std::endl; + + // Pre-allocate at least one block for each pool type + for (const auto& [poolType, config] : poolConfigs) { + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + pools[poolType] = std::vector>(); + poolIt = pools.find(poolType); + } + + auto& poolBlocks = poolIt->second; + if (poolBlocks.empty()) { + // Create initial block for this pool type + auto newBlock = createMemoryBlock(poolType, config.blockSize); + poolBlocks.push_back(std::move(newBlock)); + std::cout << " Pre-allocated block for pool type " << static_cast(poolType) << std::endl; + } + } + + std::cout << "Memory pool pre-allocation completed successfully" << std::endl; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to pre-allocate memory pools: " << e.what() << std::endl; + return false; + } +} + +void MemoryPool::setRenderingActive(bool active) { + std::lock_guard lock(poolMutex); + renderingActive = active; +} + +bool MemoryPool::isRenderingActive() const { + std::lock_guard lock(poolMutex); + return renderingActive; +} diff --git a/attachments/simple_engine/memory_pool.h b/attachments/simple_engine/memory_pool.h new file mode 100644 index 00000000..1bea7fef --- /dev/null +++ b/attachments/simple_engine/memory_pool.h @@ -0,0 +1,195 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @brief Memory pool allocator for Vulkan resources + * + * This class implements a memory pool system to reduce memory fragmentation + * and improve allocation performance by pre-allocating large chunks of memory + * and sub-allocating from them. + */ +class MemoryPool { +public: + /** + * @brief Types of memory pools based on usage patterns + */ + enum class PoolType { + VERTEX_BUFFER, // Device-local memory for vertex data + INDEX_BUFFER, // Device-local memory for index data + UNIFORM_BUFFER, // Host-visible memory for uniform data + STAGING_BUFFER, // Host-visible memory for staging operations + TEXTURE_IMAGE // Device-local memory for texture images + }; + + /** + * @brief Allocation information for a memory block + */ + struct Allocation { + vk::DeviceMemory memory; // The underlying device memory + vk::DeviceSize offset; // Offset within the memory block + vk::DeviceSize size; // Size of the allocation + uint32_t memoryTypeIndex; // Memory type index + bool isMapped; // Whether the memory is persistently mapped + void* mappedPtr; // Mapped pointer (if applicable) + }; + + /** + * @brief Memory block within a pool + */ + struct MemoryBlock { + vk::raii::DeviceMemory memory; // RAII wrapper for device memory + vk::DeviceSize size; // Total size of the block + vk::DeviceSize used; // Currently used bytes + uint32_t memoryTypeIndex; // Memory type index + bool isMapped; // Whether the block is mapped + void* mappedPtr; // Mapped pointer (if applicable) + std::vector freeList; // Free list for sub-allocations + vk::DeviceSize allocationUnit; // Size of each allocation unit + }; + +private: + const vk::raii::Device& device; + const vk::raii::PhysicalDevice& physicalDevice; + + // Pool configurations + struct PoolConfig { + vk::DeviceSize blockSize; // Size of each memory block + vk::DeviceSize allocationUnit; // Minimum allocation unit + vk::MemoryPropertyFlags properties; // Memory properties + uint32_t maxBlocks; // Maximum number of blocks + }; + + // Memory pools for different types + std::unordered_map>> pools; + std::unordered_map poolConfigs; + + // Thread safety + mutable std::mutex poolMutex; + + // Rendering state tracking to prevent pool growth during rendering + bool renderingActive = false; + + // Helper methods + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; + std::unique_ptr createMemoryBlock(PoolType poolType, vk::DeviceSize size); + MemoryBlock* findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment); + +public: + /** + * @brief Constructor + * @param device Vulkan device + * @param physicalDevice Vulkan physical device + */ + MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalDevice& physicalDevice); + + /** + * @brief Destructor + */ + ~MemoryPool(); + + /** + * @brief Initialize the memory pool with default configurations + * @return True if initialization was successful + */ + bool initialize(); + + /** + * @brief Allocate memory from a specific pool + * @param poolType Type of pool to allocate from + * @param size Size of the allocation + * @param alignment Required alignment + * @return Allocation information, or nullptr if allocation failed + */ + std::unique_ptr allocate(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment = 1); + + /** + * @brief Free a previously allocated memory block + * @param allocation The allocation to free + */ + void deallocate(std::unique_ptr allocation); + + /** + * @brief Create a buffer using pooled memory + * @param size Size of the buffer + * @param usage Buffer usage flags + * @param properties Memory properties + * @return Pair of buffer and allocation info + */ + std::pair> createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties + ); + + /** + * @brief Create an image using pooled memory + * @param width Image width + * @param height Image height + * @param format Image format + * @param tiling Image tiling + * @param usage Image usage flags + * @param properties Memory properties + * @return Pair of image and allocation info + */ + std::pair> createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties + ); + + /** + * @brief Get memory usage statistics + * @param poolType Type of pool to query + * @return Pair of (used bytes, total bytes) + */ + std::pair getMemoryUsage(PoolType poolType) const; + + /** + * @brief Get total memory usage across all pools + * @return Pair of (used bytes, total bytes) + */ + std::pair getTotalMemoryUsage() const; + + /** + * @brief Configure a specific pool type + * @param poolType Type of pool to configure + * @param blockSize Size of each memory block + * @param allocationUnit Minimum allocation unit + * @param properties Memory properties + * @param maxBlocks Maximum number of blocks + */ + void configurePool( + PoolType poolType, + vk::DeviceSize blockSize, + vk::DeviceSize allocationUnit, + vk::MemoryPropertyFlags properties, + uint32_t maxBlocks + ); + + /** + * @brief Pre-allocate memory pools to prevent allocation during rendering + * @return True if pre-allocation was successful + */ + bool preAllocatePools(); + + /** + * @brief Set rendering active state to prevent pool growth + * @param active Whether rendering is currently active + */ + void setRenderingActive(bool active); + + /** + * @brief Check if rendering is currently active + * @return True if rendering is active + */ + bool isRenderingActive() const; +}; diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index b9ca63a2..08f3e6a1 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -8,11 +8,15 @@ void MeshComponent::CreateQuad(float width, float height, const glm::vec3& color float halfWidth = width * 0.5f; float halfHeight = height * 0.5f; + // Quad facing forward (positive Z direction) + glm::vec3 normal = glm::vec3(0.0f, 0.0f, 1.0f); + glm::vec4 tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + vertices = { - { {-halfWidth, -halfHeight, 0.0f}, color, {0.0f, 0.0f} }, - { { halfWidth, -halfHeight, 0.0f}, color, {1.0f, 0.0f} }, - { { halfWidth, halfHeight, 0.0f}, color, {1.0f, 1.0f} }, - { {-halfWidth, halfHeight, 0.0f}, color, {0.0f, 1.0f} } + { {-halfWidth, -halfHeight, 0.0f}, normal, {0.0f, 0.0f}, tangent }, + { { halfWidth, -halfHeight, 0.0f}, normal, {1.0f, 0.0f}, tangent }, + { { halfWidth, halfHeight, 0.0f}, normal, {1.0f, 1.0f}, tangent }, + { {-halfWidth, halfHeight, 0.0f}, normal, {0.0f, 1.0f}, tangent } }; indices = { @@ -25,41 +29,41 @@ void MeshComponent::CreateCube(float size, const glm::vec3& color) { float halfSize = size * 0.5f; vertices = { - // Front face - { {-halfSize, -halfSize, halfSize}, color, {0.0f, 0.0f} }, - { { halfSize, -halfSize, halfSize}, color, {1.0f, 0.0f} }, - { { halfSize, halfSize, halfSize}, color, {1.0f, 1.0f} }, - { {-halfSize, halfSize, halfSize}, color, {0.0f, 1.0f} }, + // Front face (normal: +Z, tangent: +X) + { {-halfSize, -halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, -halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { {-halfSize, halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - // Back face - { {-halfSize, -halfSize, -halfSize}, color, {1.0f, 0.0f} }, - { {-halfSize, halfSize, -halfSize}, color, {1.0f, 1.0f} }, - { { halfSize, halfSize, -halfSize}, color, {0.0f, 1.0f} }, - { { halfSize, -halfSize, -halfSize}, color, {0.0f, 0.0f} }, + // Back face (normal: -Z, tangent: -X) + { {-halfSize, -halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {1.0f, 0.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, + { {-halfSize, halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {0.0f, 1.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, -halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, - // Top face - { {-halfSize, halfSize, -halfSize}, color, {0.0f, 0.0f} }, - { {-halfSize, halfSize, halfSize}, color, {0.0f, 1.0f} }, - { { halfSize, halfSize, halfSize}, color, {1.0f, 1.0f} }, - { { halfSize, halfSize, -halfSize}, color, {1.0f, 0.0f} }, + // Top face (normal: +Y, tangent: +X) + { {-halfSize, halfSize, -halfSize}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { {-halfSize, halfSize, halfSize}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, halfSize, halfSize}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, halfSize, -halfSize}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - // Bottom face - { {-halfSize, -halfSize, -halfSize}, color, {0.0f, 1.0f} }, - { { halfSize, -halfSize, -halfSize}, color, {1.0f, 1.0f} }, - { { halfSize, -halfSize, halfSize}, color, {1.0f, 0.0f} }, - { {-halfSize, -halfSize, halfSize}, color, {0.0f, 0.0f} }, + // Bottom face (normal: -Y, tangent: +X) + { {-halfSize, -halfSize, -halfSize}, {0.0f, -1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, -halfSize, -halfSize}, {0.0f, -1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { { halfSize, -halfSize, halfSize}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, + { {-halfSize, -halfSize, halfSize}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - // Right face - { { halfSize, -halfSize, -halfSize}, color, {0.0f, 0.0f} }, - { { halfSize, halfSize, -halfSize}, color, {1.0f, 0.0f} }, - { { halfSize, halfSize, halfSize}, color, {1.0f, 1.0f} }, - { { halfSize, -halfSize, halfSize}, color, {0.0f, 1.0f} }, + // Right face (normal: +X, tangent: -Z) + { { halfSize, -halfSize, -halfSize}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, + { { halfSize, halfSize, -halfSize}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, + { { halfSize, halfSize, halfSize}, {1.0f, 0.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, + { { halfSize, -halfSize, halfSize}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, - // Left face - { {-halfSize, -halfSize, -halfSize}, color, {1.0f, 0.0f} }, - { {-halfSize, -halfSize, halfSize}, color, {0.0f, 0.0f} }, - { {-halfSize, halfSize, halfSize}, color, {0.0f, 1.0f} }, - { {-halfSize, halfSize, -halfSize}, color, {1.0f, 1.0f} } + // Left face (normal: -X, tangent: +Z) + { {-halfSize, -halfSize, -halfSize}, {-1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f, 1.0f} }, + { {-halfSize, -halfSize, halfSize}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f, 1.0f} }, + { {-halfSize, halfSize, halfSize}, {-1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f} }, + { {-halfSize, halfSize, -halfSize}, {-1.0f, 0.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f} } }; indices = { diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index 6d7fd24d..9441d473 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -14,13 +14,15 @@ */ struct Vertex { glm::vec3 position; - glm::vec3 color; + glm::vec3 normal; glm::vec2 texCoord; + glm::vec4 tangent; bool operator==(const Vertex& other) const { return position == other.position && - color == other.color && - texCoord == other.texCoord; + normal == other.normal && + texCoord == other.texCoord && + tangent == other.tangent; } static vk::VertexInputBindingDescription getBindingDescription() { @@ -32,8 +34,8 @@ struct Vertex { return bindingDescription; } - static std::array getAttributeDescriptions() { - std::array attributeDescriptions = { + static std::array getAttributeDescriptions() { + std::array attributeDescriptions = { vk::VertexInputAttributeDescription{ .location = 0, .binding = 0, @@ -44,13 +46,19 @@ struct Vertex { .location = 1, .binding = 0, .format = vk::Format::eR32G32B32Sfloat, - .offset = offsetof(Vertex, color) + .offset = offsetof(Vertex, normal) }, vk::VertexInputAttributeDescription{ .location = 2, .binding = 0, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(Vertex, texCoord) + }, + vk::VertexInputAttributeDescription{ + .location = 3, + .binding = 0, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = offsetof(Vertex, tangent) } }; return attributeDescriptions; diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 9dccb1fc..85736298 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -9,32 +9,6 @@ #include // Forward declarations for classes that will be defined in separate files - -class Material { -public: - Material(const std::string& name) : name(name) {} - ~Material() = default; - - const std::string& GetName() const { return name; } - - // PBR properties - glm::vec3 albedo = glm::vec3(1.0f); - float metallic = 0.0f; - float roughness = 1.0f; - float ao = 1.0f; - glm::vec3 emissive = glm::vec3(0.0f); - - // Texture paths for PBR materials - std::string albedoTexturePath; - std::string normalTexturePath; - std::string metallicRoughnessTexturePath; - std::string occlusionTexturePath; - std::string emissiveTexturePath; - -private: - std::string name; -}; - ModelLoader::ModelLoader() { // Constructor implementation } @@ -53,7 +27,6 @@ bool ModelLoader::Initialize(Renderer* renderer) { return false; } - std::cout << "ModelLoader initialized successfully" << std::endl; return true; } @@ -77,7 +50,6 @@ Model* ModelLoader::LoadGLTF(const std::string& filename) { Model* modelPtr = model.get(); models[filename] = std::move(model); - std::cout << "Model loaded successfully: " << filename << std::endl; return modelPtr; } @@ -483,15 +455,15 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { positions[i * 3 + 2] ); - // Color (use normal as color if available, otherwise white) + // Normal (use extracted normals if available, otherwise default up) if (normals) { - vertex.color = glm::vec3( - std::abs(normals[i * 3 + 0]), - std::abs(normals[i * 3 + 1]), - std::abs(normals[i * 3 + 2]) + vertex.normal = glm::vec3( + normals[i * 3 + 0], + normals[i * 3 + 1], + normals[i * 3 + 2] ); } else { - vertex.color = glm::vec3(0.8f, 0.8f, 0.8f); + vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); // Default forward normal } // Texture coordinates @@ -504,6 +476,9 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { vertex.texCoord = glm::vec2(0.0f, 0.0f); } + // Tangent (default right tangent for now, could be extracted from GLTF if available) + vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + materialVertices[materialIndex].push_back(vertex); } } @@ -579,11 +554,6 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cerr << "Warning: Failed to extract punctual lights from " << filename << std::endl; } - // Extract lights from emissive materials - if (!ExtractEmissiveLights(gltfModel, filename)) { - std::cerr << "Warning: Failed to extract emissive lights from " << filename << std::endl; - } - std::cout << "GLTF model loaded successfully with " << combinedVertices.size() << " vertices and " << combinedIndices.size() << " indices" << std::endl; return true; } @@ -608,7 +578,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, bool success = true; - // Load albedo map + // Load albedo map or create default if (!albedoMap.empty()) { std::cout << " Loading albedo map: " << albedoMap << std::endl; material->albedoTexturePath = albedoMap; @@ -616,9 +586,13 @@ bool ModelLoader::LoadPBRTextures(Material* material, std::cerr << " Failed to load albedo texture: " << albedoMap << std::endl; success = false; } + } else { + // Use shared default albedo texture (much more efficient than creating per-material textures) + std::cout << " Using shared default albedo texture" << std::endl; + material->albedoTexturePath = renderer->SHARED_DEFAULT_ALBEDO_ID; } - // Load normal map + // Load normal map or create default if (!normalMap.empty()) { std::cout << " Loading normal map: " << normalMap << std::endl; material->normalTexturePath = normalMap; @@ -626,9 +600,13 @@ bool ModelLoader::LoadPBRTextures(Material* material, std::cerr << " Failed to load normal texture: " << normalMap << std::endl; success = false; } + } else { + // Use shared default normal texture (much more efficient than creating per-material textures) + std::cout << " Using shared default normal texture" << std::endl; + material->normalTexturePath = renderer->SHARED_DEFAULT_NORMAL_ID; } - // Load metallic-roughness map + // Load metallic-roughness map or create default if (!metallicRoughnessMap.empty()) { std::cout << " Loading metallic-roughness map: " << metallicRoughnessMap << std::endl; material->metallicRoughnessTexturePath = metallicRoughnessMap; @@ -636,9 +614,13 @@ bool ModelLoader::LoadPBRTextures(Material* material, std::cerr << " Failed to load metallic-roughness texture: " << metallicRoughnessMap << std::endl; success = false; } + } else { + // Use shared default metallic-roughness texture (much more efficient than creating per-material textures) + std::cout << " Using shared default metallic-roughness texture" << std::endl; + material->metallicRoughnessTexturePath = renderer->SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; } - // Load ambient occlusion map + // Load ambient occlusion map or create default if (!aoMap.empty()) { std::cout << " Loading ambient occlusion map: " << aoMap << std::endl; material->occlusionTexturePath = aoMap; @@ -646,9 +628,13 @@ bool ModelLoader::LoadPBRTextures(Material* material, std::cerr << " Failed to load occlusion texture: " << aoMap << std::endl; success = false; } + } else { + // Use shared default occlusion texture (much more efficient than creating per-material textures) + std::cout << " Using shared default occlusion texture" << std::endl; + material->occlusionTexturePath = renderer->SHARED_DEFAULT_OCCLUSION_ID; } - // Load emissive map + // Load emissive map or create default if (!emissiveMap.empty()) { std::cout << " Loading emissive map: " << emissiveMap << std::endl; material->emissiveTexturePath = emissiveMap; @@ -656,6 +642,10 @@ bool ModelLoader::LoadPBRTextures(Material* material, std::cerr << " Failed to load emissive texture: " << emissiveMap << std::endl; success = false; } + } else { + // Use shared default emissive texture (much more efficient than creating per-material textures) + std::cout << " Using shared default emissive texture" << std::endl; + material->emissiveTexturePath = renderer->SHARED_DEFAULT_EMISSIVE_ID; } std::cout << "PBR texture paths stored for material: " << material->GetName() << std::endl; @@ -663,25 +653,40 @@ bool ModelLoader::LoadPBRTextures(Material* material, } std::string ModelLoader::GetFirstMaterialTexturePath(const std::string& modelName) { - // Iterate through all materials to find the first one with an albedo texture path - for (const auto& materialPair : materials) { - const auto& material = materialPair.second; - if (!material->albedoTexturePath.empty()) { - std::cout << "Found texture path for model " << modelName << ": " << material->albedoTexturePath << std::endl; - return material->albedoTexturePath; - } + // Get material meshes for this specific model + auto it = materialMeshes.find(modelName); + if (it == materialMeshes.end()) { + std::cout << "No material meshes found for model: " << modelName << std::endl; + return ""; } - // If no albedo texture found, try other texture types - for (const auto& materialPair : materials) { - const auto& material = materialPair.second; - if (!material->normalTexturePath.empty()) { - std::cout << "Found normal texture path for model " << modelName << ": " << material->normalTexturePath << std::endl; - return material->normalTexturePath; + const auto& modelMaterialMeshes = it->second; + + // First, try to find a material mesh with a texture path (prioritizing base color) + for (const auto& materialMesh : modelMaterialMeshes) { + if (!materialMesh.texturePath.empty()) { + std::cout << "Found texture path for model " << modelName << ": " << materialMesh.texturePath << std::endl; + return materialMesh.texturePath; } - if (!material->metallicRoughnessTexturePath.empty()) { - std::cout << "Found metallic-roughness texture path for model " << modelName << ": " << material->metallicRoughnessTexturePath << std::endl; - return material->metallicRoughnessTexturePath; + } + + // If no texture path found in MaterialMesh, try to get from the actual materials + // Only look for albedo textures to ensure non-PBR rendering doesn't use normal/metallic maps + for (const auto& materialMesh : modelMaterialMeshes) { + const std::string& materialName = materialMesh.materialName; + if (materialName.empty()) continue; + + auto materialIt = materials.find(materialName); + if (materialIt != materials.end()) { + const auto& material = materialIt->second; + + // Only return albedo texture for non-PBR rendering compatibility + if (!material->albedoTexturePath.empty()) { + std::cout << "Found albedo texture path for model " << modelName << ": " << material->albedoTexturePath << std::endl; + return material->albedoTexturePath; + } + // Don't fall back to normal or metallic-roughness textures to avoid + // using them in non-PBR rendering where they would be incorrect } } @@ -697,12 +702,22 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m return std::vector(); } -std::vector ModelLoader::GetMaterialMeshes(const std::string& modelName) const { +const std::vector& ModelLoader::GetMaterialMeshes(const std::string& modelName) const { auto it = materialMeshes.find(modelName); if (it != materialMeshes.end()) { return it->second; } - return std::vector(); + // Return a static empty vector to avoid creating temporary objects + static const std::vector emptyVector; + return emptyVector; +} + +Material* ModelLoader::GetMaterial(const std::string& materialName) const { + auto it = materials.find(materialName); + if (it != materials.end()) { + return it->second.get(); + } + return nullptr; } bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const std::string& modelName) { @@ -724,63 +739,6 @@ bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const return true; } -bool ModelLoader::ExtractEmissiveLights(const tinygltf::Model& gltfModel, const std::string& modelName) { - std::cout << "Extracting emissive lights from model: " << modelName << std::endl; - - std::vector lights; - - // Iterate through materials to find emissive ones - for (size_t i = 0; i < gltfModel.materials.size(); ++i) { - const auto& gltfMaterial = gltfModel.materials[i]; - - // Check if material has emissive properties - bool hasEmissiveFactor = gltfMaterial.emissiveFactor.size() >= 3; - bool hasEmissiveTexture = gltfMaterial.emissiveTexture.index >= 0; - - if (!hasEmissiveFactor && !hasEmissiveTexture) { - continue; // No emissive properties - } - - // Calculate emissive intensity - glm::vec3 emissiveColor(0.0f); - if (hasEmissiveFactor) { - emissiveColor = glm::vec3( - gltfMaterial.emissiveFactor[0], - gltfMaterial.emissiveFactor[1], - gltfMaterial.emissiveFactor[2] - ); - } - - // Calculate luminance to determine if this should be a light source - float luminance = 0.299f * emissiveColor.r + 0.587f * emissiveColor.g + 0.114f * emissiveColor.b; - - // Only create lights for materials with significant emissive values - if (luminance > 0.1f) { // Threshold for creating a light - ExtractedLight light; - light.type = ExtractedLight::Type::Emissive; - light.color = emissiveColor; - light.intensity = luminance * 10.0f; // Scale up for lighting - light.range = 50.0f; // Default range for emissive lights - light.sourceMaterial = gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name; - - // For now, place the light at the origin - we'll improve this later - // by calculating positions from mesh geometry - light.position = glm::vec3(0.0f, 5.0f, 0.0f); - - lights.push_back(light); - - std::cout << " Created emissive light from material '" << light.sourceMaterial - << "' with intensity " << light.intensity << std::endl; - } - } - - // Store the extracted lights - extractedLights[modelName] = lights; - - std::cout << " Extracted " << lights.size() << " emissive lights" << std::endl; - return true; -} - std::vector ModelLoader::ParseGLTFDataOnly(const std::string& filename) { std::cout << "Thread-safe parsing GLTF file: " << filename << std::endl; @@ -939,15 +897,15 @@ std::vector ModelLoader::ParseGLTFDataOnly(const std::string& file positions[i * 3 + 2] ); - // Color (use normal as color if available, otherwise white) + // Normal (use extracted normals if available, otherwise default up) if (normals) { - vertex.color = glm::vec3( - std::abs(normals[i * 3 + 0]), - std::abs(normals[i * 3 + 1]), - std::abs(normals[i * 3 + 2]) + vertex.normal = glm::vec3( + normals[i * 3 + 0], + normals[i * 3 + 1], + normals[i * 3 + 2] ); } else { - vertex.color = glm::vec3(0.8f, 0.8f, 0.8f); + vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); // Default forward normal } // Texture coordinates @@ -960,6 +918,9 @@ std::vector ModelLoader::ParseGLTFDataOnly(const std::string& file vertex.texCoord = glm::vec2(0.0f, 0.0f); } + // Tangent (default right tangent for now, could be extracted from GLTF if available) + vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + materialVertices[materialIndex].push_back(vertex); } } @@ -985,24 +946,14 @@ std::vector ModelLoader::ParseGLTFDataOnly(const std::string& file materialMesh.vertices = vertices; materialMesh.indices = indices; - // Get texture path for this material (but don't load the texture) + // Get a texture path for this material (but don't load the texture) if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { const auto& gltfMaterial = gltfModel.materials[materialIndex]; - // Try to get base color texture first if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); } - // Fall back to other texture types if no base color - else if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { - int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; - materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); - } - else if (gltfMaterial.normalTexture.index >= 0) { - int texIndex = gltfMaterial.normalTexture.index; - materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); - } } std::cout << " Texture path: " << (materialMesh.texturePath.empty() ? "none" : materialMesh.texturePath) << " (thread-safe)" << std::endl; diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index d3b833e9..2b5e95f3 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -16,6 +16,32 @@ namespace tinygltf { class Model; } +class Material { + public: + Material(const std::string& name) : name(name) {} + ~Material() = default; + + const std::string& GetName() const { return name; } + + // PBR properties + glm::vec3 albedo = glm::vec3(1.0f); + float metallic = 0.0f; + float roughness = 1.0f; + float ao = 1.0f; + glm::vec3 emissive = glm::vec3(0.0f); + + // Texture paths for PBR materials + std::string albedoTexturePath; + std::string normalTexturePath; + std::string metallicRoughnessTexturePath; + std::string occlusionTexturePath; + std::string emissiveTexturePath; + + private: + std::string name; +}; + + /** * @brief Structure representing a light source extracted from GLTF. */ @@ -163,7 +189,14 @@ class ModelLoader { * @param modelName The name of the model. * @return Vector of material meshes from the model. */ - std::vector GetMaterialMeshes(const std::string& modelName) const; + const std::vector& GetMaterialMeshes(const std::string& modelName) const; + + /** + * @brief Get a material by name. + * @param materialName The name of the material. + * @return Pointer to the material, or nullptr if not found. + */ + Material* GetMaterial(const std::string& materialName) const; /** * @brief Parse GLTF file data without creating Vulkan resources (thread-safe). @@ -220,12 +253,4 @@ class ModelLoader { * @return True if extraction was successful, false otherwise. */ bool ExtractPunctualLights(const class tinygltf::Model& gltfModel, const std::string& modelName); - - /** - * @brief Extract lights from emissive materials. - * @param gltfModel The loaded GLTF model. - * @param modelName The name of the model. - * @return True if extraction was successful, false otherwise. - */ - bool ExtractEmissiveLights(const class tinygltf::Model& gltfModel, const std::string& modelName); }; diff --git a/attachments/simple_engine/pipeline.cpp b/attachments/simple_engine/pipeline.cpp index abeaca1f..6c632b4a 100644 --- a/attachments/simple_engine/pipeline.cpp +++ b/attachments/simple_engine/pipeline.cpp @@ -230,7 +230,7 @@ bool Pipeline::createGraphicsPipeline() { }; // Create color blend state info - std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; vk::PipelineColorBlendStateCreateInfo colorBlending{ .logicOpEnable = VK_FALSE, .logicOp = vk::LogicOp::eCopy, @@ -240,7 +240,7 @@ bool Pipeline::createGraphicsPipeline() { }; // Create dynamic state info - std::vector dynamicStates = { + std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index c0c18bb4..0d463e65 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -13,6 +13,7 @@ #include "entity.h" #include "mesh_component.h" #include "camera_component.h" +#include "memory_pool.h" // Forward declarations class ImGuiSystem; @@ -282,6 +283,20 @@ class Renderer { } vk::Format findDepthFormat(); + /** + * @brief Pre-allocate all Vulkan resources for an entity during scene loading. + * @param entity The entity to pre-allocate resources for. + * @return True if pre-allocation was successful, false otherwise. + */ + bool preAllocateEntityResources(Entity* entity); + + // Shared default PBR texture identifiers (to avoid creating hundreds of identical textures) + static const std::string SHARED_DEFAULT_ALBEDO_ID; + static const std::string SHARED_DEFAULT_NORMAL_ID; + static const std::string SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; + static const std::string SHARED_DEFAULT_OCCLUSION_ID; + static const std::string SHARED_DEFAULT_EMISSIVE_ID; + private: // Platform Platform* platform = nullptr; @@ -300,6 +315,9 @@ class Renderer { vk::raii::PhysicalDevice physicalDevice = nullptr; vk::raii::Device device = nullptr; + // Memory pool for efficient memory management + std::unique_ptr memoryPool; + // Vulkan queues vk::raii::Queue graphicsQueue = nullptr; vk::raii::Queue presentQueue = nullptr; @@ -355,7 +373,7 @@ class Renderer { // Depth buffer vk::raii::Image depthImage = nullptr; - vk::raii::DeviceMemory depthImageMemory = nullptr; + std::unique_ptr depthImageAllocation = nullptr; vk::raii::ImageView depthImageView = nullptr; // Descriptor set layouts (declared before pools and sets) @@ -365,9 +383,9 @@ class Renderer { // Mesh resources struct MeshResources { vk::raii::Buffer vertexBuffer = nullptr; - vk::raii::DeviceMemory vertexBufferMemory = nullptr; + std::unique_ptr vertexBufferAllocation = nullptr; vk::raii::Buffer indexBuffer = nullptr; - vk::raii::DeviceMemory indexBufferMemory = nullptr; + std::unique_ptr indexBufferAllocation = nullptr; uint32_t indexCount = 0; }; std::unordered_map meshResources; @@ -375,7 +393,7 @@ class Renderer { // Texture resources struct TextureResources { vk::raii::Image textureImage = nullptr; - vk::raii::DeviceMemory textureImageMemory = nullptr; + std::unique_ptr textureImageAllocation = nullptr; vk::raii::ImageView textureImageView = nullptr; vk::raii::Sampler textureSampler = nullptr; }; @@ -384,10 +402,11 @@ class Renderer { // Default texture resources (used when no texture is provided) TextureResources defaultTextureResources; + // Entity resources (contains descriptor sets - must be declared before descriptor pool) struct EntityResources { std::vector uniformBuffers; - std::vector uniformBuffersMemory; + std::vector> uniformBufferAllocations; std::vector uniformBuffersMapped; std::vector basicDescriptorSets; // For basic pipeline std::vector pbrDescriptorSets; // For PBR pipeline @@ -457,6 +476,7 @@ class Renderer { bool createTextureImageView(TextureResources& resources); bool createTextureSampler(TextureResources& resources); bool createDefaultTextureResources(); + bool createSharedDefaultPBRTextures(); bool createMeshResources(MeshComponent* meshComponent); bool createUniformBuffers(Entity* entity); bool createDescriptorPool(); @@ -483,9 +503,11 @@ class Renderer { uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); + std::pair> createBufferPooled(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size); std::pair createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties); + std::pair> createImagePooled(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties); void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout); void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 62f63f29..8d2fcbd1 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -97,6 +97,24 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + // Initialize memory pool for efficient memory management + try { + memoryPool = std::make_unique(device, physicalDevice); + if (!memoryPool->initialize()) { + std::cerr << "Failed to initialize memory pool" << std::endl; + return false; + } + + // Pre-allocate memory pools to prevent allocation during rendering + if (!memoryPool->preAllocatePools()) { + std::cerr << "Failed to pre-allocate memory pools" << std::endl; + return false; + } + } catch (const std::exception& e) { + std::cerr << "Failed to create memory pool: " << e.what() << std::endl; + return false; + } + // Create swap chain if (!createSwapChain()) { return false; @@ -160,6 +178,12 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + // Create shared default PBR textures (to avoid creating hundreds of identical textures) + if (!createSharedDefaultPBRTextures()) { + std::cerr << "Failed to create shared default PBR textures" << std::endl; + return false; + } + // Create command buffers if (!createCommandBuffers()) { return false; @@ -182,13 +206,12 @@ void Renderer::Cleanup() { // Wait for the device to be idle before cleaning up device.waitIdle(); for (auto& resources : entityResources | std::views::values) { - for (auto& memory : resources.uniformBuffersMemory) { - memory.unmapMemory(); - } + // Memory pool handles unmapping automatically, no need to manually unmap resources.basicDescriptorSets.clear(); resources.pbrDescriptorSets.clear(); resources.uniformBuffers.clear(); - resources.uniformBuffersMemory.clear(); + resources.uniformBufferAllocations.clear(); + resources.uniformBuffersMapped.clear(); } std::cout << "Renderer cleanup completed." << std::endl; initialized = false; diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 9e2e5498..fd53d9a6 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -243,7 +243,7 @@ void Renderer::cleanupSwapChain() { // Clean up depth resources depthImageView = nullptr; depthImage = nullptr; - depthImageMemory = nullptr; + depthImageAllocation = nullptr; // Clean up swap chain image views swapChainImageViews.clear(); @@ -348,30 +348,11 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera ubo.lightColors[i] = glm::vec4(light.color * light.intensity, 1.0f); } - // Fill remaining slots with dim lights if we have fewer than 4 + // Fill remaining slots with zero-intensity lights (no contribution) for (size_t i = extractedLights.size(); i < 4; ++i) { ubo.lightPositions[i] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - ubo.lightColors[i] = glm::vec4(0.1f, 0.1f, 0.1f, 1.0f); // Very dim fallback + ubo.lightColors[i] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); // No light contribution } - } else { - // Fallback to default hardcoded lights if no extracted lights available - std::cout << "No extracted lights found, using fallback lighting" << std::endl; - - // Light 0: Main key light (white, positioned above and to the right) - ubo.lightPositions[0] = glm::vec4(10.0f, 10.0f, 10.0f, 1.0f); - ubo.lightColors[0] = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); - - // Light 1: Fill light (warm, positioned to the left) - ubo.lightPositions[1] = glm::vec4(-8.0f, 5.0f, 8.0f, 1.0f); - ubo.lightColors[1] = glm::vec4(0.8f, 0.7f, 0.6f, 1.0f); - - // Light 2: Rim light (cool, positioned behind) - ubo.lightPositions[2] = glm::vec4(0.0f, 8.0f, -12.0f, 1.0f); - ubo.lightColors[2] = glm::vec4(0.6f, 0.7f, 1.0f, 1.0f); - - // Light 3: Ambient fill (dim, positioned below) - ubo.lightPositions[3] = glm::vec4(0.0f, -5.0f, 5.0f, 1.0f); - ubo.lightColors[3] = glm::vec4(0.3f, 0.3f, 0.4f, 1.0f); } // Set camera position for PBR calculations @@ -389,6 +370,21 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera // Render the scene void Renderer::Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { + // Set rendering active to prevent memory pool growth during rendering + if (memoryPool) { + memoryPool->setRenderingActive(true); + } + + // Use RAII to ensure rendering state is always reset, even if an exception occurs + struct RenderingStateGuard { + MemoryPool* pool; + RenderingStateGuard(MemoryPool* p) : pool(p) {} + ~RenderingStateGuard() { + if (pool) { + pool->setRenderingActive(false); + } + } + } guard(memoryPool.get()); // Wait for the previous frame to finish device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); @@ -495,39 +491,20 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam vk::raii::Pipeline* selectedPipeline = usePBR ? &pbrGraphicsPipeline : &graphicsPipeline; vk::raii::PipelineLayout* selectedLayout = usePBR ? &pbrPipelineLayout : &pipelineLayout; - // Get the mesh resources + // Get the mesh resources - they should already exist from pre-allocation auto meshIt = meshResources.find(meshComponent); if (meshIt == meshResources.end()) { - // Create mesh resources if they don't exist - if (!createMeshResources(meshComponent)) { - continue; - } - meshIt = meshResources.find(meshComponent); + std::cerr << "ERROR: Mesh resources not found for entity " << entity->GetName() + << " - resources should have been pre-allocated during scene loading!" << std::endl; + continue; } - // Get the entity resources + // Get the entity resources - they should already exist from pre-allocation auto entityIt = entityResources.find(entity); if (entityIt == entityResources.end()) { - // Create entity resources if they don't exist - if (!createUniformBuffers(entity)) { - continue; - } - - // Create descriptor sets with correct pipeline type - if (!createDescriptorSets(entity, meshComponent->GetTexturePath(), usePBR)) { - continue; - } - - entityIt = entityResources.find(entity); - } else { - // Entity exists, but check if we need to create descriptor sets for the current pipeline type - auto& targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; - if (targetDescriptorSets.empty()) { - // Create missing descriptor sets for the current pipeline type - if (!createDescriptorSets(entity, meshComponent->GetTexturePath(), usePBR)) { - continue; - } - } + std::cerr << "ERROR: Entity resources not found for entity " << entity->GetName() + << " - resources should have been pre-allocated during scene loading!" << std::endl; + continue; } // Bind pipeline if it changed @@ -535,10 +512,6 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); currentPipeline = selectedPipeline; currentLayout = selectedLayout; - - if (usePBR) { - std::cout << "Using PBR pipeline for entity: " << entity->GetName() << std::endl; - } } // Update the uniform buffer @@ -571,28 +544,59 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {}); - // Push constants for PBR pipeline - if (usePBR) { - // Define PBR push constants structure matching the shader - struct PushConstants { - glm::vec4 baseColorFactor; - float metallicFactor; - float roughnessFactor; - int baseColorTextureSet; - int physicalDescriptorTextureSet; - int normalTextureSet; - int occlusionTextureSet; - int emissiveTextureSet; - float alphaMask; - float alphaMaskCutoff; - }; + // Define PBR push constants structure matching the shader + struct PushConstants { + glm::vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; + }; - // Set default PBR material properties + // Set PBR material properties using push constants + if (usePBR) { PushConstants pushConstants{}; - pushConstants.baseColorFactor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); // White base color - pushConstants.metallicFactor = 0.0f; // Non-metallic - pushConstants.roughnessFactor = 1.0f; // Fully rough - pushConstants.baseColorTextureSet = 0; // Use texture set 0 + + // Try to get material properties for this specific entity + if (modelLoader && entity->GetName().find("Bistro_Material_") == 0) { + // Extract material name from entity name for Bistro entities + std::string entityName = entity->GetName(); + size_t materialStart = entityName.find("Bistro_Material_"); + if (materialStart != std::string::npos) { + // Try to extract material name from entity name + size_t nameStart = entityName.find_last_of("_"); + if (nameStart != std::string::npos && nameStart < entityName.length() - 1) { + std::string materialName = entityName.substr(nameStart + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + // Use actual PBR properties from the GLTF material + pushConstants.baseColorFactor = glm::vec4(material->albedo, 1.0f); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + } else { + // Fallback: Use entity-specific variation + size_t hash = std::hash{}(entityName); + float variation = (hash % 100) / 100.0f; + pushConstants.baseColorFactor = glm::vec4(0.7f + variation * 0.3f, 0.6f + variation * 0.4f, 0.5f + variation * 0.5f, 1.0f); + pushConstants.metallicFactor = variation * 0.8f; + pushConstants.roughnessFactor = 0.2f + variation * 0.6f; + } + } + } + } else { + // Default PBR material properties for non-Bistro entities + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + } + + // Set texture binding indices + pushConstants.baseColorTextureSet = 0; pushConstants.physicalDescriptorTextureSet = 0; pushConstants.normalTextureSet = 0; pushConstants.occlusionTextureSet = 0; @@ -609,7 +613,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam ); } - // Draw the mesh + // Draw the entity once (no redundant draw calls) commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, 1, 0, 0, 0); } diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 3a9d15af..66b57803 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1,4 +1,5 @@ #include "renderer.h" +#include "model_loader.h" #include #include #include @@ -11,14 +12,21 @@ // This file contains resource-related methods from the Renderer class +// Define shared default PBR texture identifiers (static constants) +const std::string Renderer::SHARED_DEFAULT_ALBEDO_ID = "__shared_default_albedo__"; +const std::string Renderer::SHARED_DEFAULT_NORMAL_ID = "__shared_default_normal__"; +const std::string Renderer::SHARED_DEFAULT_METALLIC_ROUGHNESS_ID = "__shared_default_metallic_roughness__"; +const std::string Renderer::SHARED_DEFAULT_OCCLUSION_ID = "__shared_default_occlusion__"; +const std::string Renderer::SHARED_DEFAULT_EMISSIVE_ID = "__shared_default_emissive__"; + // Create depth resources bool Renderer::createDepthResources() { try { // Find depth format vk::Format depthFormat = findDepthFormat(); - // Create depth image - auto [depthImg, depthImgMem] = createImage( + // Create depth image using memory pool + auto [depthImg, depthImgAllocation] = createImagePooled( swapChainExtent.width, swapChainExtent.height, depthFormat, @@ -28,7 +36,7 @@ bool Renderer::createDepthResources() { ); depthImage = std::move(depthImg); - depthImageMemory = std::move(depthImgMem); + depthImageAllocation = std::move(depthImgAllocation); // Create depth image view depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); @@ -83,8 +91,8 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc // Free pixel data stbi_image_free(pixels); - // Create texture image - auto [textureImg, textureImgMem] = createImage( + // Create texture image using memory pool + auto [textureImg, textureImgAllocation] = createImagePooled( texWidth, texHeight, vk::Format::eR8G8B8A8Srgb, @@ -94,7 +102,7 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc ); resources.textureImage = std::move(textureImg); - resources.textureImageMemory = std::move(textureImgMem); + resources.textureImageAllocation = std::move(textureImgAllocation); // Transition image layout for copy transitionImageLayout( @@ -155,6 +163,54 @@ bool Renderer::createTextureImageView(TextureResources& resources) { } } +// Create shared default PBR textures (to avoid creating hundreds of identical textures) +bool Renderer::createSharedDefaultPBRTextures() { + try { + std::cout << "Creating shared default PBR textures..." << std::endl; + + // Create shared default albedo texture (neutral gray to reduce excessive brightness) + unsigned char whitePixel[4] = {128, 128, 128, 255}; // 50% gray instead of pure white + if (!LoadTextureFromMemory(SHARED_DEFAULT_ALBEDO_ID, whitePixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default albedo texture" << std::endl; + return false; + } + + // Create shared default normal texture (flat normal) + unsigned char normalPixel[4] = {128, 128, 255, 255}; // (0.5, 0.5, 1.0, 1.0) in 0-255 range + if (!LoadTextureFromMemory(SHARED_DEFAULT_NORMAL_ID, normalPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default normal texture" << std::endl; + return false; + } + + // Create shared default metallic-roughness texture (non-metallic, fully rough) + unsigned char metallicRoughnessPixel[4] = {0, 255, 0, 255}; // (unused, roughness=1.0, metallic=0.0, alpha=1.0) + if (!LoadTextureFromMemory(SHARED_DEFAULT_METALLIC_ROUGHNESS_ID, metallicRoughnessPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default metallic-roughness texture" << std::endl; + return false; + } + + // Create shared default occlusion texture (white - no occlusion) + unsigned char occlusionPixel[4] = {255, 255, 255, 255}; + if (!LoadTextureFromMemory(SHARED_DEFAULT_OCCLUSION_ID, occlusionPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default occlusion texture" << std::endl; + return false; + } + + // Create shared default emissive texture (black - no emission) + unsigned char emissivePixel[4] = {0, 0, 0, 255}; + if (!LoadTextureFromMemory(SHARED_DEFAULT_EMISSIVE_ID, emissivePixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default emissive texture" << std::endl; + return false; + } + + std::cout << "Successfully created all shared default PBR textures" << std::endl; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create shared default PBR textures: " << e.what() << std::endl; + return false; + } +} + // Create default texture resources (1x1 white texture) bool Renderer::createDefaultTextureResources() { try { @@ -177,8 +233,8 @@ bool Renderer::createDefaultTextureResources() { memcpy(data, pixels.data(), static_cast(imageSize)); stagingBufferMemory.unmapMemory(); - // Create texture image - auto [textureImg, textureImgMem] = createImage( + // Create texture image using memory pool + auto [textureImg, textureImgAllocation] = createImagePooled( width, height, vk::Format::eR8G8B8A8Srgb, @@ -188,7 +244,7 @@ bool Renderer::createDefaultTextureResources() { ); defaultTextureResources.textureImage = std::move(textureImg); - defaultTextureResources.textureImageMemory = std::move(textureImgMem); + defaultTextureResources.textureImageAllocation = std::move(textureImgAllocation); // Transition image layout for copy transitionImageLayout( @@ -359,8 +415,8 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne stagingBufferMemory.unmapMemory(); - // Create texture image - auto [textureImg, textureImgMem] = createImage( + // Create texture image using memory pool + auto [textureImg, textureImgAllocation] = createImagePooled( width, height, vk::Format::eR8G8B8A8Srgb, @@ -370,7 +426,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne ); resources.textureImage = std::move(textureImg); - resources.textureImageMemory = std::move(textureImgMem); + resources.textureImageAllocation = std::move(textureImgAllocation); // Transition image layout for copy transitionImageLayout( @@ -452,8 +508,8 @@ bool Renderer::createMeshResources(MeshComponent* meshComponent) { memcpy(vertexData, vertices.data(), static_cast(vertexBufferSize)); stagingVertexBufferMemory.unmapMemory(); - // Create vertex buffer on device - auto [vertexBuffer, vertexBufferMemory] = createBuffer( + // Create vertex buffer on device using memory pool + auto [vertexBuffer, vertexBufferAllocation] = createBufferPooled( vertexBufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal @@ -475,8 +531,8 @@ bool Renderer::createMeshResources(MeshComponent* meshComponent) { memcpy(indexData, indices.data(), static_cast(indexBufferSize)); stagingIndexBufferMemory.unmapMemory(); - // Create index buffer on device - auto [indexBuffer, indexBufferMemory] = createBuffer( + // Create index buffer on device using memory pool + auto [indexBuffer, indexBufferAllocation] = createBufferPooled( indexBufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal @@ -488,9 +544,9 @@ bool Renderer::createMeshResources(MeshComponent* meshComponent) { // Create mesh resources MeshResources resources; resources.vertexBuffer = std::move(vertexBuffer); - resources.vertexBufferMemory = std::move(vertexBufferMemory); + resources.vertexBufferAllocation = std::move(vertexBufferAllocation); resources.indexBuffer = std::move(indexBuffer); - resources.indexBufferMemory = std::move(indexBufferMemory); + resources.indexBufferAllocation = std::move(indexBufferAllocation); resources.indexCount = static_cast(indices.size()); // Add to mesh resources map @@ -503,6 +559,122 @@ bool Renderer::createMeshResources(MeshComponent* meshComponent) { } } +// REMOVED: Create material-specific mesh resources for PBR rendering - Memory inefficient approach +// This function was creating separate vertex/index buffers for each material, multiplying memory usage +/* +bool Renderer::createMaterialMeshResources(const std::string& modelName) { + try { + // Check if material mesh resources already exist + auto it = materialMeshResources.find(modelName); + if (it != materialMeshResources.end()) { + return true; + } + + // Get material meshes from model loader + if (!modelLoader) { + std::cerr << "ModelLoader not available for creating material mesh resources" << std::endl; + return false; + } + + std::vector materialMeshes = modelLoader->GetMaterialMeshes(modelName); + if (materialMeshes.empty()) { + std::cerr << "No material meshes found for model: " << modelName << std::endl; + return false; + } + + std::cout << "Creating material mesh resources for model: " << modelName + << " (" << materialMeshes.size() << " materials)" << std::endl; + + // Create material mesh resources + MaterialMeshResources resources; + resources.materialMeshes.reserve(materialMeshes.size()); + resources.materialNames.reserve(materialMeshes.size()); + resources.materialIndices.reserve(materialMeshes.size()); + + for (const auto& materialMesh : materialMeshes) { + const auto& vertices = materialMesh.vertices; + const auto& indices = materialMesh.indices; + + if (vertices.empty() || indices.empty()) { + std::cerr << "Material mesh has no vertices or indices: " << materialMesh.materialName << std::endl; + continue; + } + + std::cout << " Creating buffers for material: " << materialMesh.materialName + << " (" << vertices.size() << " vertices, " << indices.size() << " indices)" << std::endl; + + // Create vertex buffer + vk::DeviceSize vertexBufferSize = sizeof(vertices[0]) * vertices.size(); + auto [stagingVertexBuffer, stagingVertexBufferMemory] = createBuffer( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy vertex data to staging buffer + void* vertexData = stagingVertexBufferMemory.mapMemory(0, vertexBufferSize); + memcpy(vertexData, vertices.data(), static_cast(vertexBufferSize)); + stagingVertexBufferMemory.unmapMemory(); + + // Create vertex buffer on device + auto [vertexBuffer, vertexBufferMemory] = createBuffer( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Copy from staging buffer to device buffer + copyBuffer(stagingVertexBuffer, vertexBuffer, vertexBufferSize); + + // Create index buffer + vk::DeviceSize indexBufferSize = sizeof(indices[0]) * indices.size(); + auto [stagingIndexBuffer, stagingIndexBufferMemory] = createBuffer( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy index data to staging buffer + void* indexData = stagingIndexBufferMemory.mapMemory(0, indexBufferSize); + memcpy(indexData, indices.data(), static_cast(indexBufferSize)); + stagingIndexBufferMemory.unmapMemory(); + + // Create index buffer on device + auto [indexBuffer, indexBufferMemory] = createBuffer( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Copy from staging buffer to device buffer + copyBuffer(stagingIndexBuffer, indexBuffer, indexBufferSize); + + // Create mesh resources for this material + MeshResources meshRes; + meshRes.vertexBuffer = std::move(vertexBuffer); + meshRes.vertexBufferMemory = std::move(vertexBufferMemory); + meshRes.indexBuffer = std::move(indexBuffer); + meshRes.indexBufferMemory = std::move(indexBufferMemory); + meshRes.indexCount = static_cast(indices.size()); + + // Store the mesh resources and metadata + resources.materialMeshes.emplace_back(std::move(meshRes)); + resources.materialNames.emplace_back(materialMesh.materialName); + resources.materialIndices.emplace_back(materialMesh.materialIndex); + } + + // Add to material mesh resources map + materialMeshResources[modelName] = std::move(resources); + + std::cout << "Successfully created material mesh resources for model: " << modelName << std::endl; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create material mesh resources: " << e.what() << std::endl; + return false; + } +} +*/ + // Create uniform buffers bool Renderer::createUniformBuffers(Entity* entity) { try { @@ -515,19 +687,23 @@ bool Renderer::createUniformBuffers(Entity* entity) { // Create entity resources EntityResources resources; - // Create uniform buffers + // Create uniform buffers using memory pool vk::DeviceSize bufferSize = sizeof(UniformBufferObject); for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - auto [buffer, bufferMemory] = createBuffer( + auto [buffer, bufferAllocation] = createBufferPooled( bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); - void* mappedMemory = bufferMemory.mapMemory(0, bufferSize); + // Use the memory pool's mapped pointer if available + void* mappedMemory = bufferAllocation->mappedPtr; + if (!mappedMemory) { + std::cerr << "Warning: Uniform buffer allocation is not mapped" << std::endl; + } resources.uniformBuffers.emplace_back(std::move(buffer)); - resources.uniformBuffersMemory.emplace_back(std::move(bufferMemory)); + resources.uniformBufferAllocations.emplace_back(std::move(bufferAllocation)); resources.uniformBuffersMapped.emplace_back(mappedMemory); } @@ -771,13 +947,105 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa } } -// Create buffer +// Pre-allocate all Vulkan resources for an entity during scene loading +bool Renderer::preAllocateEntityResources(Entity* entity) { + try { + std::cout << "Pre-allocating resources for entity: " << entity->GetName() << std::endl; + + // Get the mesh component + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { + std::cerr << "Entity " << entity->GetName() << " has no mesh component" << std::endl; + return false; + } + + // 1. Create mesh resources (vertex/index buffers) + if (!createMeshResources(meshComponent)) { + std::cerr << "Failed to create mesh resources for entity: " << entity->GetName() << std::endl; + return false; + } + + // 2. Create uniform buffers + if (!createUniformBuffers(entity)) { + std::cerr << "Failed to create uniform buffers for entity: " << entity->GetName() << std::endl; + return false; + } + + // 3. Pre-allocate BOTH basic and PBR descriptor sets + std::string texturePath = meshComponent->GetTexturePath(); + + // Create basic descriptor sets + if (!createDescriptorSets(entity, texturePath, false)) { + std::cerr << "Failed to create basic descriptor sets for entity: " << entity->GetName() << std::endl; + return false; + } + + // Create PBR descriptor sets + if (!createDescriptorSets(entity, texturePath, true)) { + std::cerr << "Failed to create PBR descriptor sets for entity: " << entity->GetName() << std::endl; + return false; + } + + std::cout << "Successfully pre-allocated all resources for entity: " << entity->GetName() << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Failed to pre-allocate resources for entity " << entity->GetName() << ": " << e.what() << std::endl; + return false; + } +} + +// Create buffer using memory pool for efficient allocation +std::pair> Renderer::createBufferPooled( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + try { + if (!memoryPool) { + throw std::runtime_error("Memory pool not initialized"); + } + + // Use memory pool for allocation + auto [buffer, allocation] = memoryPool->createBuffer(size, usage, properties); + std::cout << "Created buffer using memory pool: " << size << " bytes" << std::endl; + + return {std::move(buffer), std::move(allocation)}; + + } catch (const std::exception& e) { + std::cerr << "Failed to create buffer with memory pool: " << e.what() << std::endl; + throw; + } +} + +// Legacy createBuffer function - now strictly enforces memory pool usage std::pair Renderer::createBuffer( vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties) { + + // This function should only be used for temporary staging buffers during resource creation + // All persistent resources should use createBufferPooled directly + + if (!memoryPool) { + throw std::runtime_error("Memory pool not available - cannot create buffer"); + } + + // Check if we're trying to allocate during rendering + if (memoryPool->isRenderingActive()) { + std::cerr << "ERROR: Attempted to create buffer during rendering! Size: " << size << " bytes" << std::endl; + std::cerr << "This violates the constraint that no new memory should be allocated during rendering." << std::endl; + throw std::runtime_error("Buffer creation attempted during rendering - this is not allowed"); + } + + // Only allow direct allocation for staging buffers (temporary, host-visible) + if (!(properties & vk::MemoryPropertyFlagBits::eHostVisible)) { + std::cerr << "ERROR: Legacy createBuffer should only be used for staging buffers!" << std::endl; + throw std::runtime_error("Legacy createBuffer used for non-staging buffer"); + } + try { - // Create buffer + std::cout << "Creating staging buffer with direct allocation: " << size << " bytes" << std::endl; + vk::BufferCreateInfo bufferInfo{ .size = size, .usage = usage, @@ -786,7 +1054,7 @@ std::pair Renderer::createBuffer( vk::raii::Buffer buffer(device, bufferInfo); - // Allocate memory + // Allocate memory directly for staging buffers only vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo{ .allocationSize = memRequirements.size, @@ -799,8 +1067,9 @@ std::pair Renderer::createBuffer( buffer.bindMemory(*bufferMemory, 0); return {std::move(buffer), std::move(bufferMemory)}; + } catch (const std::exception& e) { - std::cerr << "Failed to create buffer: " << e.what() << std::endl; + std::cerr << "Failed to create staging buffer: " << e.what() << std::endl; throw; } } @@ -899,6 +1168,31 @@ std::pair Renderer::createImage( } } +// Create image using memory pool for efficient allocation +std::pair> Renderer::createImagePooled( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties) { + try { + if (!memoryPool) { + throw std::runtime_error("Memory pool not initialized"); + } + + // Use memory pool for allocation + auto [image, allocation] = memoryPool->createImage(width, height, format, tiling, usage, properties); + std::cout << "Created image using memory pool: " << width << "x" << height << " format=" << static_cast(format) << std::endl; + + return {std::move(image), std::move(allocation)}; + + } catch (const std::exception& e) { + std::cerr << "Failed to create image with memory pool: " << e.what() << std::endl; + throw; + } +} + // Create image view vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags) { try { diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index c2a8e5a7..0d94a101 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -90,7 +90,7 @@ void CreateEntitiesFromLoadedMaterials(Engine* engine) { auto* transform = materialEntity->AddComponent(); transform->SetPosition(glm::vec3(0.0f, -1.5f, 0.0f)); transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); - transform->SetScale(glm::vec3(0.1f, 0.1f, 0.1f)); + transform->SetScale(glm::vec3(1.0f, 1.0f, 1.0f)); // Add mesh component with material-specific data auto* mesh = materialEntity->AddComponent(); @@ -108,6 +108,12 @@ void CreateEntitiesFromLoadedMaterials(Engine* engine) { << " vertices, no texture" << std::endl; } + // Pre-allocate all Vulkan resources for this Bistro entity + if (!renderer->preAllocateEntityResources(materialEntity)) { + std::cerr << "Failed to pre-allocate resources for Bistro entity: " << entityName << std::endl; + // Continue with other entities even if one fails + } + entitiesCreated++; g_loadingState.materialsLoaded = entitiesCreated; } else { diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 6b2a37b7..b86244a6 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -11,7 +11,7 @@ struct VSInput { // Output from vertex shader / Input to fragment shader struct VSOutput { float4 Position : SV_POSITION; - float3 WorldPos : POSITION; + float3 WorldPos; float3 Normal : NORMAL; float2 UV : TEXCOORD0; float4 Tangent : TANGENT; @@ -182,9 +182,9 @@ float4 PSMain(VSOutput input) : SV_TARGET Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; } - // Add ambient and emissive - float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; - float3 color = ambient + Lo + emissive; + // Add only emissive (no hardcoded ambient - use only model-defined lights) + // Scale down emissive contribution to reduce excessive brightness + float3 color = Lo + emissive * 0.1; // HDR tonemapping and gamma correction color = color / (color + float3(1.0, 1.0, 1.0)); diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang index 70fae67f..20457330 100644 --- a/attachments/simple_engine/shaders/texturedMesh.slang +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -1,17 +1,19 @@ // Combined vertex and fragment shader for textured mesh rendering -// This shader provides basic textured rendering with a uniform color +// This shader provides basic textured rendering with simple lighting // Input from vertex buffer struct VSInput { float3 Position : POSITION; - float3 Color : COLOR; + float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; }; // Output from vertex shader / Input to fragment shader struct VSOutput { float4 Position : SV_POSITION; - float3 Color : COLOR; + float3 WorldPos; + float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; }; @@ -33,10 +35,12 @@ VSOutput VSMain(VSInput input) VSOutput output; // Transform position to clip space - output.Position = mul(ubo.proj, mul(ubo.view, mul(ubo.model, float4(input.Position, 1.0)))); + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); - // Pass color and texture coordinates to fragment shader - output.Color = input.Color; + // Pass world position and transformed normal to fragment shader + output.WorldPos = worldPos.xyz; + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); output.TexCoord = input.TexCoord; return output; @@ -46,6 +50,24 @@ VSOutput VSMain(VSInput input) [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET { - // Sample texture and multiply by color - return texSampler.Sample(input.TexCoord) * float4(input.Color, 1.0); + // Sample the texture + float4 texColor = texSampler.Sample(input.TexCoord); + + // Simple directional lighting + float3 lightDir = normalize(float3(0.5, 1.0, 0.3)); // Fixed light direction + float3 normal = normalize(input.Normal); + float lightIntensity = max(dot(normal, lightDir), 0.2); // Minimum ambient of 0.2 + + // Check if texture is pure white (indicates no meaningful texture data) + float whiteness = (texColor.r + texColor.g + texColor.b) / 3.0; + bool isPureWhite = whiteness > 0.95; // Threshold for "pure white" + + if (isPureWhite) { + // No texture or pure white texture: use a default color with lighting + float3 defaultColor = float3(0.8, 0.8, 0.8); // Light gray + return float4(defaultColor * lightIntensity, 1.0); + } else { + // Apply simple lighting to texture + return float4(texColor.rgb * lightIntensity, texColor.a); + } } From b998a13ad93fcb167d8889c9859947303b954e65 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 29 Jul 2025 01:09:21 -0700 Subject: [PATCH 032/102] fix for formating the list. --- en/Building_a_Simple_Engine/Tooling/02_cicd.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc index 47f344ee..a3e975cc 100644 --- a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc +++ b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc @@ -217,9 +217,9 @@ Once your application passes all tests, the final stage is packaging and distrib * Include the appropriate Vulkan loader for each platform * Package shader files or pre-compiled SPIR-V * Consider using platform-specific packaging tools: - * Windows: NSIS, WiX, or MSIX - * Linux: AppImage, Flatpak, or .deb/.rpm packages - * macOS: DMG or App Store packages + ** Windows: NSIS, WiX, or MSIX + ** Linux: AppImage, Flatpak, or .deb/.rpm packages + ** macOS: DMG or App Store packages ==== Handling Vulkan Dependencies From 8be59f4ecd529fba8928f5ed47d086661ef802fe Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 29 Jul 2025 02:15:37 -0700 Subject: [PATCH 033/102] PBR is working. KTX2 with BasisU is working. Shadows are working. Gamma and exposure are working as sliders in the UI. Need to cleanup and Physics and then we're feature complete. --- attachments/simple_engine/CMakeLists.txt | 1 + attachments/simple_engine/engine.cpp | 234 ++++- attachments/simple_engine/engine.h | 24 +- attachments/simple_engine/imgui_system.cpp | 76 +- attachments/simple_engine/imgui_system.h | 2 +- attachments/simple_engine/main.cpp | 13 +- attachments/simple_engine/memory_pool.cpp | 78 +- attachments/simple_engine/mesh_component.h | 30 +- attachments/simple_engine/model_loader.cpp | 938 +++++++++++++----- attachments/simple_engine/model_loader.h | 64 +- attachments/simple_engine/physics_system.cpp | 197 ++-- attachments/simple_engine/physics_system.h | 13 +- attachments/simple_engine/pipeline.h | 20 +- attachments/simple_engine/renderer.h | 192 +++- attachments/simple_engine/renderer_core.cpp | 109 +- .../simple_engine/renderer_pipelines.cpp | 80 +- .../simple_engine/renderer_rendering.cpp | 88 +- .../simple_engine/renderer_resources.cpp | 796 ++++++++++----- .../simple_engine/renderer_shadows.cpp | 140 +++ attachments/simple_engine/scene_loading.cpp | 352 +++++-- attachments/simple_engine/scene_loading.h | 34 +- .../simple_engine/shaders/lighting.slang | 10 +- attachments/simple_engine/shaders/pbr.slang | 214 +++- .../simple_engine/shaders/texturedMesh.slang | 2 + 24 files changed, 2667 insertions(+), 1040 deletions(-) create mode 100644 attachments/simple_engine/renderer_shadows.cpp diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 3c519986..55ea1295 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -97,6 +97,7 @@ set(SOURCES renderer_compute.cpp renderer_utils.cpp renderer_resources.cpp + renderer_shadows.cpp memory_pool.cpp resource_manager.cpp entity.cpp diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 2c13e363..54a81f02 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -40,40 +40,59 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Set mouse callback platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { - // Handle camera rotation when left mouse button is pressed - if (buttons & 1) { // Left mouse button (bit 0) - if (!cameraControl.mouseLeftPressed) { - cameraControl.mouseLeftPressed = true; - cameraControl.firstMouse = true; + // Check if ImGui wants to capture mouse input first + bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + + if (!imguiWantsMouse) { + // Handle mouse click for poke functionality (right mouse button) + if (buttons & 2) { // Right mouse button (bit 1) + if (!cameraControl.mouseRightPressed) { + cameraControl.mouseRightPressed = true; + // Perform poke on mouse click + HandleMousePoke(x, y); + } + } else { + cameraControl.mouseRightPressed = false; } - if (cameraControl.firstMouse) { + // Handle camera rotation when left mouse button is pressed + if (buttons & 1) { // Left mouse button (bit 0) + if (!cameraControl.mouseLeftPressed) { + cameraControl.mouseLeftPressed = true; + cameraControl.firstMouse = true; + } + + if (cameraControl.firstMouse) { + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + cameraControl.firstMouse = false; + } + + float xOffset = x - cameraControl.lastMouseX; + float yOffset = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top cameraControl.lastMouseX = x; cameraControl.lastMouseY = y; - cameraControl.firstMouse = false; - } - - float xOffset = x - cameraControl.lastMouseX; - float yOffset = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top - cameraControl.lastMouseX = x; - cameraControl.lastMouseY = y; - xOffset *= cameraControl.mouseSensitivity; - yOffset *= cameraControl.mouseSensitivity; + xOffset *= cameraControl.mouseSensitivity; + yOffset *= cameraControl.mouseSensitivity; - cameraControl.yaw += xOffset; - cameraControl.pitch += yOffset; + cameraControl.yaw += xOffset; + cameraControl.pitch += yOffset; - // Constrain pitch to avoid gimbal lock - if (cameraControl.pitch > 89.0f) cameraControl.pitch = 89.0f; - if (cameraControl.pitch < -89.0f) cameraControl.pitch = -89.0f; - } else { - cameraControl.mouseLeftPressed = false; + // Constrain pitch to avoid gimbal lock + if (cameraControl.pitch > 89.0f) cameraControl.pitch = 89.0f; + if (cameraControl.pitch < -89.0f) cameraControl.pitch = -89.0f; + } else { + cameraControl.mouseLeftPressed = false; + } } if (imguiSystem) { imguiSystem->HandleMouse(x, y, buttons); } + + // Always perform hover detection (even when ImGui is active) + HandleMouseHover(x, y); }); // Set keyboard callback @@ -104,6 +123,7 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool case GLFW_KEY_PAGE_DOWN: cameraControl.moveDown = pressed; break; + default: break; } if (imguiSystem) { @@ -137,7 +157,7 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool return false; } - // Initialize physics system + // Initialize a physics system physicsSystem->SetRenderer(renderer.get()); if (!physicsSystem->Initialize()) { return false; @@ -148,7 +168,7 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool return false; } - // Connect ImGui system to audio system for UI controls + // Connect ImGui system to an audio system for UI controls imguiSystem->SetAudioSystem(audioSystem.get()); initialized = true; @@ -207,19 +227,16 @@ void Engine::Cleanup() { Entity* Engine::CreateEntity(const std::string& name) { // Check if an entity with this name already exists - if (entityMap.find(name) != entityMap.end()) { + if (entityMap.contains(name)) { return nullptr; } // Create the entity auto entity = std::make_unique(name); - Entity* entityPtr = entity.get(); - // Add to the map and vector - entityMap[name] = entityPtr; entities.push_back(std::move(entity)); - return entityPtr; + return entities.back().get(); } Entity* Engine::GetEntity(const std::string& name) { @@ -310,10 +327,7 @@ ImGuiSystem* Engine::GetImGuiSystem() const { } void Engine::Update(float deltaTime) { - // Check for completed background loading and create entities if ready - CheckAndCreateLoadedEntities(); - - // Update physics system + // Update a physics system physicsSystem->Update(deltaTime); // Update audio system @@ -380,7 +394,7 @@ float Engine::CalculateDeltaTime() { return delta / 1000.0f; // Convert to seconds } -void Engine::HandleResize(int width, int height) { +void Engine::HandleResize(int width, int height) const { // Update the active camera's aspect ratio if (activeCamera) { activeCamera->SetAspectRatio(static_cast(width) / static_cast(height)); @@ -397,11 +411,11 @@ void Engine::HandleResize(int width, int height) { } } -void Engine::UpdateCameraControls(float deltaTime) { +void Engine::UpdateCameraControls(float deltaTime) const { if (!activeCamera) return; - // Get camera transform component - TransformComponent* cameraTransform = activeCamera->GetOwner()->GetComponent(); + // Get a camera transform component + auto* cameraTransform = activeCamera->GetOwner()->GetComponent(); if (!cameraTransform) return; // Calculate movement speed @@ -409,16 +423,16 @@ void Engine::UpdateCameraControls(float deltaTime) { // Calculate camera direction vectors based on yaw and pitch glm::vec3 front; - front.x = cos(glm::radians(cameraControl.yaw)) * cos(glm::radians(cameraControl.pitch)); - front.y = sin(glm::radians(cameraControl.pitch)); - front.z = sin(glm::radians(cameraControl.yaw)) * cos(glm::radians(cameraControl.pitch)); + front.x = cosf(glm::radians(cameraControl.yaw)) * cosf(glm::radians(cameraControl.pitch)); + front.y = sinf(glm::radians(cameraControl.pitch)); + front.z = sinf(glm::radians(cameraControl.yaw)) * cosf(glm::radians(cameraControl.pitch)); front = glm::normalize(front); glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 right = glm::normalize(glm::cross(front, up)); up = glm::normalize(glm::cross(right, front)); - // Get current camera position + // Get the current camera position glm::vec3 position = cameraTransform->GetPosition(); // Apply movement based on input @@ -444,25 +458,139 @@ void Engine::UpdateCameraControls(float deltaTime) { // Update camera position cameraTransform->SetPosition(position); - // Update camera target based on direction + // Update camera target based on a direction glm::vec3 target = position + front; activeCamera->SetTarget(target); } -void Engine::CheckAndCreateLoadedEntities() { - // Check if background loading is complete - if (g_loadingState.loadingComplete && !g_loadingState.loadedMaterials.empty()) { - // Create entities from loaded materials on the main thread - CreateEntitiesFromLoadedMaterials(this); +void Engine::HandleMousePoke(float mouseX, float mouseY) const { + if (!activeCamera || !physicsSystem) { + return; + } + + // Get window dimensions + int windowWidth, windowHeight; + platform->GetWindowSize(&windowWidth, &windowHeight); + + // Convert mouse coordinates to normalized device coordinates (-1 to 1) + float ndcX = (2.0f * mouseX) / static_cast(windowWidth) - 1.0f; + float ndcY = 1.0f - (2.0f * mouseY) / static_cast(windowHeight); + + // Get camera matrices + glm::mat4 viewMatrix = activeCamera->GetViewMatrix(); + glm::mat4 projMatrix = activeCamera->GetProjectionMatrix(); + + // Calculate inverse matrices + glm::mat4 invView = glm::inverse(viewMatrix); + glm::mat4 invProj = glm::inverse(projMatrix); + + // Convert NDC to world space + glm::vec4 rayClip = glm::vec4(ndcX, ndcY, -1.0f, 1.0f); + glm::vec4 rayEye = invProj * rayClip; + rayEye = glm::vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); + glm::vec4 rayWorld = invView * rayEye; - // Reset the loading complete flag - g_loadingState.loadingComplete = false; + // Get ray origin and direction + glm::vec3 rayOrigin = activeCamera->GetPosition(); + glm::vec3 rayDirection = glm::normalize(glm::vec3(rayWorld)); + + // Perform raycast + glm::vec3 hitPosition; + glm::vec3 hitNormal; + Entity* hitEntity = nullptr; + + if (physicsSystem->Raycast(rayOrigin, rayDirection, 1000.0f, &hitPosition, &hitNormal, &hitEntity)) { + if (hitEntity) { + std::cout << "Mouse poke hit entity: " << hitEntity->GetName() << std::endl; + + // Find or create rigid body for the entity + RigidBody* rigidBody = nullptr; + + // Check if entity already has a rigid body (this is a simplified approach) + // In a real implementation, you'd have a component system to track this + rigidBody = physicsSystem->CreateRigidBody(hitEntity, CollisionShape::Box, 1.0f); + + if (rigidBody) { + // Apply a small impulse in the direction of the ray + glm::vec3 impulse = rayDirection * 0.5f; // Small force magnitude as requested + rigidBody->ApplyImpulse(impulse, glm::vec3(0.0f)); + + std::cout << "Applied poke impulse to " << hitEntity->GetName() << std::endl; + } + } + } else { + std::cout << "Mouse poke missed - no entity hit" << std::endl; + } +} + +void Engine::HandleMouseHover(float mouseX, float mouseY) { + if (!activeCamera || !physicsSystem) { + return; } - // Check for loading errors - if (g_loadingState.loadingFailed) { - std::cerr << "Background loading failed: " << g_loadingState.errorMessage << std::endl; - g_loadingState.loadingFailed = false; // Reset the flag + // Update current mouse position + currentMouseX = mouseX; + currentMouseY = mouseY; + + // Get window dimensions + int windowWidth, windowHeight; + platform->GetWindowSize(&windowWidth, &windowHeight); + + // Convert mouse coordinates to normalized device coordinates (-1 to 1) + float ndcX = (2.0f * mouseX) / static_cast(windowWidth) - 1.0f; + float ndcY = 1.0f - (2.0f * mouseY) / static_cast(windowHeight); + + // Get camera matrices + glm::mat4 viewMatrix = activeCamera->GetViewMatrix(); + glm::mat4 projMatrix = activeCamera->GetProjectionMatrix(); + + // Calculate inverse matrices + glm::mat4 invView = glm::inverse(viewMatrix); + glm::mat4 invProj = glm::inverse(projMatrix); + + // Convert NDC to world space + glm::vec4 rayClip = glm::vec4(ndcX, ndcY, -1.0f, 1.0f); + glm::vec4 rayEye = invProj * rayClip; + rayEye = glm::vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); + glm::vec4 rayWorld = invView * rayEye; + + // Get ray origin and direction + glm::vec3 rayOrigin = activeCamera->GetPosition(); + glm::vec3 rayDirection = glm::normalize(glm::vec3(rayWorld)); + + // Perform raycast + glm::vec3 hitPosition; + glm::vec3 hitNormal; + Entity* hitEntity = nullptr; + + if (physicsSystem->Raycast(rayOrigin, rayDirection, 1000.0f, &hitPosition, &hitNormal, &hitEntity)) { + if (hitEntity) { + // Check if this entity is pokeable (has "_SMALL_POKEABLE" suffix) + std::string entityName = hitEntity->GetName(); + + if (entityName.find("_SMALL_POKEABLE") != std::string::npos) { + // Update a hovered entity if it's different from the current one + if (hoveredEntity != hitEntity) { + hoveredEntity = hitEntity; + renderer->SetHighlightedEntity(hoveredEntity); + std::cout << "Now hovering over pokeable entity: " << entityName << std::endl; + } + } else { + // Clear hover if we're over a non-pokeable entity + if (hoveredEntity != nullptr) { + std::cout << "No longer hovering over pokeable entity" << std::endl; + hoveredEntity = nullptr; + renderer->SetHighlightedEntity(nullptr); + } + } + } + } else { + // Clear hover if no entity is hit + if (hoveredEntity != nullptr) { + std::cout << "No longer hovering over pokeable entity" << std::endl; + hoveredEntity = nullptr; + renderer->SetHighlightedEntity(nullptr); + } } } diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index cadd5d1e..25887865 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -191,6 +191,7 @@ class Engine { bool moveUp = false; bool moveDown = false; bool mouseLeftPressed = false; + bool mouseRightPressed = false; float lastMouseX = 0.0f; float lastMouseY = 0.0f; float yaw = 0.0f; // Horizontal rotation @@ -200,6 +201,11 @@ class Engine { float mouseSensitivity = 0.1f; } cameraControl; + // Hover state tracking + Entity* hoveredEntity = nullptr; + float currentMouseX = 0.0f; + float currentMouseY = 0.0f; + /** * @brief Update the engine state. * @param deltaTime The time elapsed since the last update. @@ -222,16 +228,26 @@ class Engine { * @param width The new width of the window. * @param height The new height of the window. */ - void HandleResize(int width, int height); + void HandleResize(int width, int height) const; /** * @brief Update camera controls based on input state. * @param deltaTime The time elapsed since the last update. */ - void UpdateCameraControls(float deltaTime); + void UpdateCameraControls(float deltaTime) const; /** - * @brief Check for completed background loading and create entities if ready. + * @brief Handle mouse poke interaction to apply forces to clicked objects. + * @param mouseX The x-coordinate of the mouse click. + * @param mouseY The y-coordinate of the mouse click. */ - void CheckAndCreateLoadedEntities(); + void HandleMousePoke(float mouseX, float mouseY) const; + + /** + * @brief Handle mouse hover detection for highlighting pokeable entities. + * @param mouseX The x-coordinate of the mouse position. + * @param mouseY The y-coordinate of the mouse position. + */ + void HandleMouseHover(float mouseX, float mouseY); + }; diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index c293f97e..f87d4aa6 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -109,22 +109,82 @@ void ImGuiSystem::NewFrame() { // Create HRTF Audio Control UI ImGui::Begin("HRTF Audio Controls"); ImGui::Text("Hello, Vulkan!"); - // PBR Rendering Controls + // Lighting Controls - BRDF/PBR is now the default lighting model ImGui::Separator(); - ImGui::Text("PBR Rendering Controls:"); + ImGui::Text("Lighting Controls:"); - if (ImGui::Checkbox("Enable PBR Rendering", &pbrEnabled)) { - std::cout << "PBR rendering " << (pbrEnabled ? "enabled" : "disabled") << std::endl; + // Invert the checkbox logic - now controls basic lighting instead of PBR + bool useBasicLighting = !pbrEnabled; + if (ImGui::Checkbox("Use Basic Lighting (Phong)", &useBasicLighting)) { + pbrEnabled = !useBasicLighting; + std::cout << "Lighting mode: " << (pbrEnabled ? "BRDF/PBR (default)" : "Basic Phong") << std::endl; } if (pbrEnabled) { - ImGui::Text("Status: PBR pipeline active for supported models"); - ImGui::Text("Models using PBR: Bistro scene"); + ImGui::Text("Status: BRDF/PBR pipeline active (default)"); + ImGui::Text("All models rendered with physically-based lighting"); } else { - ImGui::Text("Status: Using basic rendering pipeline"); - ImGui::Text("All models rendered with basic shading"); + ImGui::Text("Status: Basic Phong pipeline active"); + ImGui::Text("All models rendered with basic Phong shading"); } + // BRDF Quality Controls - always available since BRDF is now default + ImGui::Separator(); + ImGui::Text("BRDF Quality Controls:"); + + // Gamma correction slider + static float gamma = 2.2f; + if (ImGui::SliderFloat("Gamma Correction", &gamma, 1.0f, 3.0f, "%.2f")) { + // Update gamma in renderer + if (renderer) { + renderer->SetGamma(gamma); + } + std::cout << "Gamma set to: " << gamma << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("Reset##Gamma")) { + gamma = 2.2f; + if (renderer) { + renderer->SetGamma(gamma); + } + std::cout << "Gamma reset to: " << gamma << std::endl; + } + + // Exposure slider + static float exposure = 3.0f; // Higher default for emissive lighting + if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 10.0f, "%.2f")) { + // Update exposure in renderer + if (renderer) { + renderer->SetExposure(exposure); + } + std::cout << "Exposure set to: " << exposure << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("Reset##Exposure")) { + exposure = 3.0f; // Reset to higher value for emissive lighting + if (renderer) { + renderer->SetExposure(exposure); + } + std::cout << "Exposure reset to: " << exposure << std::endl; + } + + // Shadow toggle + static bool shadowsEnabled = true; // Default shadows on + if (ImGui::Checkbox("Enable Shadows", &shadowsEnabled)) { + // Update shadows in renderer + if (renderer) { + renderer->SetShadowsEnabled(shadowsEnabled); + } + std::cout << "Shadows " << (shadowsEnabled ? "enabled" : "disabled") << std::endl; + } + + ImGui::Text("Tip: Adjust gamma if scene looks too dark/bright"); + ImGui::Text("Tip: Adjust exposure if scene looks washed out"); + if (!pbrEnabled) { + ImGui::Text("Note: Quality controls affect BRDF rendering only"); + } + + ImGui::Separator(); ImGui::Text("3D Audio Position Control"); // Audio source selection diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index 56f16406..80dbb268 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -154,7 +154,7 @@ class ImGuiSystem { bool initialized = false; // PBR rendering state - bool pbrEnabled = false; + bool pbrEnabled = true; /** * @brief Create Vulkan resources for ImGui. diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index 18f246b3..2b57b64b 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -1,12 +1,10 @@ #include "engine.h" #include "transform_component.h" -#include "mesh_component.h" #include "camera_component.h" #include "scene_loading.h" #include #include -#include // Constants constexpr int WINDOW_WIDTH = 800; @@ -36,13 +34,10 @@ void SetupScene(Engine* engine) { // Set the camera as the active camera engine->SetActiveCamera(camera); - // Start loading Bistro.glb model in background thread - if (ModelLoader* modelLoader = engine->GetModelLoader()) { - std::cout << "Starting threaded loading of Bistro model..." << std::endl; - std::thread loadingThread(LoadBistroModelAsync, modelLoader); - loadingThread.detach(); // Let the thread run independently - std::cout << "Background loading thread started. Application will continue running..." << std::endl; - } + // Load GLTF model synchronously on the main thread + std::cout << "Loading GLTF model synchronously..." << std::endl; + LoadGLTFModel(engine, "../Assets/bistro_gltf/bistro.gltf"); + std::cout << "GLTF model loading completed." << std::endl; } #if PLATFORM_ANDROID diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp index 118d276d..a00a4037 100644 --- a/attachments/simple_engine/memory_pool.cpp +++ b/attachments/simple_engine/memory_pool.cpp @@ -1,7 +1,6 @@ #include "memory_pool.h" #include #include -#include MemoryPool::MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalDevice& physicalDevice) : device(device), physicalDevice(physicalDevice) { @@ -9,32 +8,32 @@ MemoryPool::MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalD MemoryPool::~MemoryPool() { // RAII will handle cleanup automatically - std::lock_guard lock(poolMutex); + std::lock_guard lock(poolMutex); pools.clear(); } bool MemoryPool::initialize() { - std::lock_guard lock(poolMutex); + std::lock_guard lock(poolMutex); try { // Configure default pool settings based on typical usage patterns - // Vertex buffer pool: Large allocations, device-local + // Vertex buffer pool: Large allocations, device-local (increased for large models like bistro) configurePool( PoolType::VERTEX_BUFFER, - 64 * 1024 * 1024, // 64MB blocks + 128 * 1024 * 1024, // 128MB blocks (doubled) 4096, // 4KB allocation units vk::MemoryPropertyFlagBits::eDeviceLocal, - 8 // Max 8 blocks (512MB total) + 16 // Max 16 blocks (2GB total, quadrupled) ); - // Index buffer pool: Medium allocations, device-local + // Index buffer pool: Medium allocations, device-local (increased for large models like bistro) configurePool( PoolType::INDEX_BUFFER, - 32 * 1024 * 1024, // 32MB blocks + 64 * 1024 * 1024, // 64MB blocks (doubled) 2048, // 2KB allocation units vk::MemoryPropertyFlagBits::eDeviceLocal, - 4 // Max 4 blocks (128MB total) + 8 // Max 8 blocks (512MB total, quadrupled) ); // Uniform buffer pool: Small allocations, host-visible @@ -55,13 +54,13 @@ bool MemoryPool::initialize() { 4 // Max 4 blocks (64MB total) ); - // Texture image pool: Large allocations, device-local + // Texture image pool: Large allocations, device-local (significantly increased for large models like bistro) configurePool( PoolType::TEXTURE_IMAGE, - 128 * 1024 * 1024, // 128MB blocks + 256 * 1024 * 1024, // 256MB blocks (doubled) 4096, // 4KB allocation units vk::MemoryPropertyFlagBits::eDeviceLocal, - 8 // Max 8 blocks (1GB total) + 24 // Max 24 blocks (6GB total, 6x increase) ); return true; @@ -72,11 +71,11 @@ bool MemoryPool::initialize() { } void MemoryPool::configurePool( - PoolType poolType, - vk::DeviceSize blockSize, - vk::DeviceSize allocationUnit, - vk::MemoryPropertyFlags properties, - uint32_t maxBlocks) { + const PoolType poolType, + const vk::DeviceSize blockSize, + const vk::DeviceSize allocationUnit, + const vk::MemoryPropertyFlags properties, + const uint32_t maxBlocks) { PoolConfig config; config.blockSize = blockSize; @@ -87,8 +86,8 @@ void MemoryPool::configurePool( poolConfigs[poolType] = config; } -uint32_t MemoryPool::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { - vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); +uint32_t MemoryPool::findMemoryType(const uint32_t typeFilter, const vk::MemoryPropertyFlags properties) const { + const vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { if ((typeFilter & (1 << i)) && @@ -108,8 +107,8 @@ std::unique_ptr MemoryPool::createMemoryBlock(PoolType const PoolConfig& config = configIt->second; - // Use the larger of requested size or configured block size - vk::DeviceSize blockSize = std::max(size, config.blockSize); + // Use the larger of the requested size or configured block size + const vk::DeviceSize blockSize = std::max(size, config.blockSize); // Create a dummy buffer to get memory requirements for the memory type vk::BufferCreateInfo bufferInfo{ @@ -151,8 +150,8 @@ std::unique_ptr MemoryPool::createMemoryBlock(PoolType block->mappedPtr = nullptr; } - // Initialize free list - size_t numUnits = static_cast(blockSize / config.allocationUnit); + // Initialize a free list + const size_t numUnits = blockSize / config.allocationUnit; block->freeList.resize(numUnits, true); // All units initially free @@ -170,11 +169,11 @@ MemoryPool::MemoryBlock* MemoryPool::findSuitableBlock(PoolType poolType, vk::De const PoolConfig& config = poolConfigs[poolType]; // Calculate required units (accounting for alignment) - vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; - size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); + const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + const size_t requiredUnits = (alignedSize + config.allocationUnit - 1) / config.allocationUnit; // Search existing blocks for sufficient free space - for (auto& block : poolBlocks) { + for (const auto& block : poolBlocks) { // Find consecutive free units size_t consecutiveFree = 0; for (size_t i = 0; i < block->freeList.size(); ++i) { @@ -200,11 +199,10 @@ MemoryPool::MemoryBlock* MemoryPool::findSuitableBlock(PoolType poolType, vk::De try { auto newBlock = createMemoryBlock(poolType, alignedSize); - MemoryBlock* blockPtr = newBlock.get(); poolBlocks.push_back(std::move(newBlock)); std::cout << "Created new memory block during initialization (pool type: " << static_cast(poolType) << ")" << std::endl; - return blockPtr; + return poolBlocks.back().get(); } catch (const std::exception& e) { std::cerr << "Failed to create new memory block: " << e.what() << std::endl; return nullptr; @@ -226,8 +224,8 @@ std::unique_ptr MemoryPool::allocate(PoolType poolType, const PoolConfig& config = poolConfigs[poolType]; // Calculate required units (accounting for alignment) - vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; - size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); + const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + const size_t requiredUnits = (alignedSize + config.allocationUnit - 1) / config.allocationUnit; // Find consecutive free units size_t startUnit = 0; @@ -287,8 +285,8 @@ void MemoryPool::deallocate(std::unique_ptr allocation) { for (auto& block : poolBlocks) { if (*block->memory == allocation->memory) { // Calculate which units to free - size_t startUnit = static_cast(allocation->offset / config.allocationUnit); - size_t numUnits = static_cast((allocation->size + config.allocationUnit - 1) / config.allocationUnit); + size_t startUnit = allocation->offset / config.allocationUnit; + size_t numUnits = (allocation->size + config.allocationUnit - 1) / config.allocationUnit; // Mark units as free for (size_t i = startUnit; i < startUnit + numUnits; ++i) { @@ -307,12 +305,12 @@ void MemoryPool::deallocate(std::unique_ptr allocation) { } std::pair> MemoryPool::createBuffer( - vk::DeviceSize size, - vk::BufferUsageFlags usage, - vk::MemoryPropertyFlags properties) { + const vk::DeviceSize size, + const vk::BufferUsageFlags usage, + const vk::MemoryPropertyFlags properties) { - // Determine pool type based on usage and properties - PoolType poolType; + // Determine a pool type based on usage and properties + PoolType poolType = PoolType::VERTEX_BUFFER; if (usage & vk::BufferUsageFlagBits::eVertexBuffer) { poolType = PoolType::VERTEX_BUFFER; } else if (usage & vk::BufferUsageFlagBits::eIndexBuffer) { @@ -321,12 +319,10 @@ std::pair> MemoryPool: poolType = PoolType::UNIFORM_BUFFER; } else if (properties & vk::MemoryPropertyFlagBits::eHostVisible) { poolType = PoolType::STAGING_BUFFER; - } else { - poolType = PoolType::VERTEX_BUFFER; // Default to vertex buffer pool } // Create the buffer - vk::BufferCreateInfo bufferInfo{ + const vk::BufferCreateInfo bufferInfo{ .size = size, .usage = usage, .sharingMode = vk::SharingMode::eExclusive @@ -455,7 +451,7 @@ bool MemoryPool::preAllocatePools() { } void MemoryPool::setRenderingActive(bool active) { - std::lock_guard lock(poolMutex); + std::lock_guard lock(poolMutex); renderingActive = active; } diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index 9441d473..cdeb7196 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -72,7 +72,14 @@ class MeshComponent : public Component { private: std::vector vertices; std::vector indices; - std::string texturePath; + + // All PBR texture paths for this mesh + std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility + std::string baseColorTexturePath; // Base color (albedo) texture + std::string normalTexturePath; // Normal map texture + std::string metallicRoughnessTexturePath; // Metallic-roughness texture + std::string occlusionTexturePath; // Ambient occlusion texture + std::string emissiveTexturePath; // Emissive texture // Vulkan resources will be managed by the renderer // This component only stores the data @@ -97,7 +104,7 @@ class MeshComponent : public Component { * @brief Get the vertices of the mesh. * @return The vertices. */ - const std::vector& GetVertices() const { + [[nodiscard]] const std::vector& GetVertices() const { return vertices; } @@ -113,7 +120,7 @@ class MeshComponent : public Component { * @brief Get the indices of the mesh. * @return The indices. */ - const std::vector& GetIndices() const { + [[nodiscard]] const std::vector& GetIndices() const { return indices; } @@ -123,16 +130,31 @@ class MeshComponent : public Component { */ void SetTexturePath(const std::string& path) { texturePath = path; + baseColorTexturePath = path; // Keep baseColor in sync for backward compatibility } /** * @brief Get the texture path for the mesh. * @return The path to the texture file. */ - const std::string& GetTexturePath() const { + [[nodiscard]] const std::string& GetTexturePath() const { return texturePath; } + // PBR texture path setters + void SetBaseColorTexturePath(const std::string& path) { baseColorTexturePath = path; } + void SetNormalTexturePath(const std::string& path) { normalTexturePath = path; } + void SetMetallicRoughnessTexturePath(const std::string& path) { metallicRoughnessTexturePath = path; } + void SetOcclusionTexturePath(const std::string& path) { occlusionTexturePath = path; } + void SetEmissiveTexturePath(const std::string& path) { emissiveTexturePath = path; } + + // PBR texture path getters + [[nodiscard]] const std::string& GetBaseColorTexturePath() const { return baseColorTexturePath; } + [[nodiscard]] const std::string& GetNormalTexturePath() const { return normalTexturePath; } + [[nodiscard]] const std::string& GetMetallicRoughnessTexturePath() const { return metallicRoughnessTexturePath; } + [[nodiscard]] const std::string& GetOcclusionTexturePath() const { return occlusionTexturePath; } + [[nodiscard]] const std::string& GetEmissiveTexturePath() const { return emissiveTexturePath; } + /** * @brief Create a simple quad mesh. * @param width The width of the quad. diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 85736298..e25e4d14 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -3,15 +3,14 @@ #include "mesh_component.h" #include #include +#include #include // Include stb_image for proper texture loading (implementation is in renderer_resources.cpp) #include -// Forward declarations for classes that will be defined in separate files -ModelLoader::ModelLoader() { - // Constructor implementation -} +// Emissive scaling factor to convert from Blender units to engine units +#define EMISSIVE_SCALE_FACTOR (1.0f / 638.0f) ModelLoader::~ModelLoader() { // Destructor implementation @@ -47,10 +46,9 @@ Model* ModelLoader::LoadGLTF(const std::string& filename) { } // Store the model - Model* modelPtr = model.get(); models[filename] = std::move(model); - return modelPtr; + return models[filename].get(); } Model* ModelLoader::LoadGLTFWithPBR(const std::string& filename, @@ -86,11 +84,9 @@ Model* ModelLoader::LoadGLTFWithPBR(const std::string& filename, materials[material->GetName()] = std::move(material); // Store the model - Model* modelPtr = model.get(); models[filename] = std::move(model); - std::cout << "Model with PBR materials loaded successfully: " << filename << std::endl; - return modelPtr; + return models[filename].get(); } Model* ModelLoader::GetModel(const std::string& name) { @@ -121,19 +117,26 @@ Material* ModelLoader::CreatePBRMaterial(const std::string& name, material->metallic = metallic; material->roughness = roughness; material->ao = ao; - material->emissive = emissive; + material->emissive = emissive * EMISSIVE_SCALE_FACTOR; // Store the material - Material* materialPtr = material.get(); materials[name] = std::move(material); std::cout << "PBR material created successfully: " << name << std::endl; - return materialPtr; + return materials[name].get(); } bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cout << "Parsing GLTF file: " << filename << std::endl; + // Extract the directory path from the model file to use as base path for textures + std::filesystem::path modelPath(filename); + std::string baseTexturePath = modelPath.parent_path().string(); + if (!baseTexturePath.empty() && baseTexturePath.back() != '/') { + baseTexturePath += "/"; + } + std::cout << "Using base texture path: " << baseTexturePath << std::endl; + // Create tinygltf loader tinygltf::Model gltfModel; tinygltf::TinyGLTF loader; @@ -170,7 +173,6 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Free stb_image data stbi_image_free(data); - std::cout << "Loaded texture: " << width << "x" << height << " with " << channels << " channels" << std::endl; return true; }, nullptr); @@ -196,8 +198,6 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { return false; } - std::cout << "Successfully loaded GLTF file with " << gltfModel.meshes.size() << " meshes" << std::endl; - // Extract mesh data from the first mesh (for now, we'll handle multiple meshes later) if (gltfModel.meshes.empty()) { std::cerr << "No meshes found in GLTF file" << std::endl; @@ -205,10 +205,8 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } // Process materials first - std::cout << "Processing " << gltfModel.materials.size() << " materials" << std::endl; for (size_t i = 0; i < gltfModel.materials.size(); ++i) { const auto& gltfMaterial = gltfModel.materials[i]; - std::cout << " Material " << i << ": " << gltfMaterial.name << std::endl; // Create PBR material auto material = std::make_unique(gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name); @@ -221,17 +219,30 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { gltfMaterial.pbrMetallicRoughness.baseColorFactor[2] ); } - material->metallic = gltfMaterial.pbrMetallicRoughness.metallicFactor; - material->roughness = gltfMaterial.pbrMetallicRoughness.roughnessFactor; + material->metallic = static_cast(gltfMaterial.pbrMetallicRoughness.metallicFactor); + material->roughness = static_cast(gltfMaterial.pbrMetallicRoughness.roughnessFactor); if (gltfMaterial.emissiveFactor.size() >= 3) { material->emissive = glm::vec3( gltfMaterial.emissiveFactor[0], gltfMaterial.emissiveFactor[1], gltfMaterial.emissiveFactor[2] - ); + ) * EMISSIVE_SCALE_FACTOR; } + // Parse KHR_materials_emissive_strength extension + auto extensionIt = gltfMaterial.extensions.find("KHR_materials_emissive_strength"); + if (extensionIt != gltfMaterial.extensions.end()) { + const tinygltf::Value& extension = extensionIt->second; + if (extension.Has("emissiveStrength") && extension.Get("emissiveStrength").IsNumber()) { + material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()) * EMISSIVE_SCALE_FACTOR; + } + } else { + // Default emissive strength is 1.0 according to GLTF spec, scaled for engine units + material->emissiveStrength = 1.0f * EMISSIVE_SCALE_FACTOR; + } + + // Extract texture information and load embedded texture data if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; @@ -241,16 +252,31 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = "gltf_texture_" + std::to_string(texIndex); material->albedoTexturePath = textureId; - // Load embedded texture data + // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; + std::cout << " Image data size: " << image.image.size() << ", URI: " << image.uri << std::endl; if (!image.image.empty()) { + // Load embedded texture data + std::cout << " Loading embedded base color texture: " << textureId << std::endl; if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded base color texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; + std::cout << " Successfully loaded embedded base color texture: " << textureId << std::endl; } else { - std::cerr << " Failed to load base color texture: " << textureId << std::endl; + std::cerr << " Failed to load embedded base color texture: " << textureId << std::endl; } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + std::cout << " Loading external base color texture: " << texturePath << std::endl; + if (renderer->LoadTexture(texturePath)) { + // Update the material to use the external texture path instead of gltf_texture_X + material->albedoTexturePath = texturePath; + std::cout << " Successfully loaded external base color texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external base color texture: " << texturePath << std::endl; + } + } else { + std::cout << " No image data or URI available, skipping texture loading" << std::endl; } } } @@ -264,15 +290,25 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = "gltf_texture_" + std::to_string(texIndex); material->metallicRoughnessTexturePath = textureId; - // Load embedded texture data + // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { + // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded metallic-roughness texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; + std::cout << " Successfully loaded embedded metallic-roughness texture: " << textureId << std::endl; + } else { + std::cerr << " Failed to load embedded metallic-roughness texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the material to use the external texture path instead of gltf_texture_X + material->metallicRoughnessTexturePath = texturePath; + std::cout << " Successfully loaded external metallic-roughness texture: " << texturePath << std::endl; } else { - std::cerr << " Failed to load metallic-roughness texture: " << textureId << std::endl; + std::cerr << " Failed to load external metallic-roughness texture: " << texturePath << std::endl; } } } @@ -287,15 +323,26 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = "gltf_texture_" + std::to_string(texIndex); material->normalTexturePath = textureId; - // Load embedded texture data + // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { + // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded normal texture: " << textureId + std::cout << " Successfully loaded embedded normal texture: " << textureId << " (" << image.width << "x" << image.height << ")" << std::endl; } else { - std::cerr << " Failed to load normal texture: " << textureId << std::endl; + std::cerr << " Failed to load embedded normal texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the material to use the external texture path instead of gltf_texture_X + material->normalTexturePath = texturePath; + std::cout << " Successfully loaded external normal texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external normal texture: " << texturePath << std::endl; } } } @@ -310,15 +357,26 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = "gltf_texture_" + std::to_string(texIndex); material->occlusionTexturePath = textureId; - // Load embedded texture data + // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { + // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded occlusion texture: " << textureId + std::cout << " Successfully loaded embedded occlusion texture: " << textureId << " (" << image.width << "x" << image.height << ")" << std::endl; } else { - std::cerr << " Failed to load occlusion texture: " << textureId << std::endl; + std::cerr << " Failed to load embedded occlusion texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the material to use the external texture path instead of gltf_texture_X + material->occlusionTexturePath = texturePath; + std::cout << " Successfully loaded external occlusion texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external occlusion texture: " << texturePath << std::endl; } } } @@ -333,15 +391,26 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = "gltf_texture_" + std::to_string(texIndex); material->emissiveTexturePath = textureId; - // Load embedded texture data + // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { + // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded emissive texture: " << textureId + std::cout << " Successfully loaded embedded emissive texture: " << textureId << " (" << image.width << "x" << image.height << ")" << std::endl; } else { - std::cerr << " Failed to load emissive texture: " << textureId << std::endl; + std::cerr << " Failed to load embedded emissive texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the material to use the external texture path instead of gltf_texture_X + material->emissiveTexturePath = texturePath; + std::cout << " Successfully loaded external emissive texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external emissive texture: " << texturePath << std::endl; } } } @@ -352,6 +421,70 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { materials[material->GetName()] = std::move(material); } + // Process cameras from GLTF file + if (!gltfModel.cameras.empty()) { + std::cout << "Found " << gltfModel.cameras.size() << " camera(s) in GLTF file" << std::endl; + + for (size_t i = 0; i < gltfModel.cameras.size(); ++i) { + const auto& gltfCamera = gltfModel.cameras[i]; + std::cout << " Camera " << i << ": " << gltfCamera.name << std::endl; + + // Store camera data in the model for later use + CameraData cameraData; + cameraData.name = gltfCamera.name.empty() ? ("camera_" + std::to_string(i)) : gltfCamera.name; + + if (gltfCamera.type == "perspective") { + cameraData.isPerspective = true; + cameraData.fov = static_cast(gltfCamera.perspective.yfov); + cameraData.aspectRatio = static_cast(gltfCamera.perspective.aspectRatio); + cameraData.nearPlane = static_cast(gltfCamera.perspective.znear); + cameraData.farPlane = static_cast(gltfCamera.perspective.zfar); + std::cout << " Perspective camera: FOV=" << cameraData.fov + << ", Aspect=" << cameraData.aspectRatio + << ", Near=" << cameraData.nearPlane + << ", Far=" << cameraData.farPlane << std::endl; + } else if (gltfCamera.type == "orthographic") { + cameraData.isPerspective = false; + cameraData.orthographicSize = static_cast(gltfCamera.orthographic.ymag); + cameraData.nearPlane = static_cast(gltfCamera.orthographic.znear); + cameraData.farPlane = static_cast(gltfCamera.orthographic.zfar); + std::cout << " Orthographic camera: Size=" << cameraData.orthographicSize + << ", Near=" << cameraData.nearPlane + << ", Far=" << cameraData.farPlane << std::endl; + } + + // Find the node that uses this camera to get transform information + for (size_t nodeIdx = 0; nodeIdx < gltfModel.nodes.size(); ++nodeIdx) { + const auto& node = gltfModel.nodes[nodeIdx]; + if (node.camera == static_cast(i)) { + // Extract transform from node + if (node.translation.size() == 3) { + cameraData.position = glm::vec3( + static_cast(node.translation[0]), + static_cast(node.translation[1]), + static_cast(node.translation[2]) + ); + } + + if (node.rotation.size() == 4) { + cameraData.rotation = glm::quat( + static_cast(node.rotation[3]), // w + static_cast(node.rotation[0]), // x + static_cast(node.rotation[1]), // y + static_cast(node.rotation[2]) // z + ); + } + + std::cout << " Position: (" << cameraData.position.x << ", " + << cameraData.position.y << ", " << cameraData.position.z << ")" << std::endl; + break; + } + } + + model->cameras.push_back(cameraData); + } + } + // Group primitives by material to create separate meshes for each material std::map> materialVertices; std::map> materialIndices; @@ -369,7 +502,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } // Initialize vectors for this material if not already done - if (materialVertices.find(materialIndex) == materialVertices.end()) { + if (!materialVertices.contains(materialIndex)) { materialVertices[materialIndex] = std::vector(); materialIndices[materialIndex] = std::vector(); @@ -396,12 +529,12 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Handle different index types if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - const uint16_t* buf = static_cast(indexData); + const auto* buf = static_cast(indexData); for (size_t i = 0; i < indexAccessor.count; ++i) { materialIndices[materialIndex].push_back(buf[i] + indexOffset); } } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - const uint32_t* buf = static_cast(indexData); + const auto* buf = static_cast(indexData); for (size_t i = 0; i < indexAccessor.count; ++i) { materialIndices[materialIndex].push_back(buf[i] + indexOffset); } @@ -419,7 +552,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; const tinygltf::Buffer& posBuffer = gltfModel.buffers[posBufferView.buffer]; - const float* positions = reinterpret_cast( + const auto* positions = reinterpret_cast( &posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset]); // Get texture coordinates (if available) @@ -506,27 +639,281 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { materialMesh.vertices = vertices; materialMesh.indices = indices; - // Get texture path for this material + // Get ALL texture paths for this material (same as ParseGLTFDataOnly) if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { const auto& gltfMaterial = gltfModel.materials[materialIndex]; - // Try to get base color texture first + // Extract base color texture if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; - materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; // Keep for backward compatibility + + // Load texture data (embedded or external) + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + // Load embedded texture data + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded embedded baseColor texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load embedded baseColor texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the MaterialMesh to use the external texture path instead of gltf_texture_X + materialMesh.baseColorTexturePath = texturePath; + materialMesh.texturePath = texturePath; // Keep for backward compatibility + std::cout << " Loaded external baseColor texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external baseColor texture: " << texturePath << std::endl; + } + } + } + } + } else { + // Since texture indices are -1, try to find external texture files by material name + std::string materialName = materialNames[materialIndex]; + + // Look for external texture files that match this specific material + for (const auto & image : gltfModel.images) { + if (!image.uri.empty()) { + std::string imageUri = image.uri; + + // Check if this image belongs to this specific material based on naming patterns + // Look for BaseColor/Albedo textures that match the material name + if ((imageUri.find("BaseColor") != std::string::npos || + imageUri.find("Albedo") != std::string::npos || + imageUri.find("Diffuse") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { + + // Use the relative path from the GLTF directory + std::string texturePath = baseTexturePath + imageUri; + materialMesh.baseColorTexturePath = texturePath; + materialMesh.texturePath = texturePath; // Keep for backward compatibility + std::cout << " Found external baseColor texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } + } + + // Extract normal texture + if (gltfMaterial.normalTexture.index >= 0) { + int texIndex = gltfMaterial.normalTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.normalTexturePath = textureId; + + // Load texture data (embedded or external) + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + // Load embedded texture data + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded embedded normal texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load embedded normal texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the MaterialMesh to use the external texture path instead of gltf_texture_X + materialMesh.normalTexturePath = texturePath; + std::cout << " Loaded external normal texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external normal texture: " << texturePath << std::endl; + } + } + } + } + } else { + // Look for external normal texture files that match this specific material + std::string materialName = materialNames[materialIndex]; + for (const auto & image : gltfModel.images) { + if (!image.uri.empty()) { + std::string imageUri = image.uri; + if (imageUri.find("Normal") != std::string::npos && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { + std::string texturePath = baseTexturePath + imageUri; + materialMesh.normalTexturePath = texturePath; + std::cout << " Found external normal texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } } - // Fall back to other texture types if no base color - else if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { + + // Extract metallic-roughness texture + if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; - materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.metallicRoughnessTexturePath = textureId; + + // Load texture data (embedded or external) + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + // Load embedded texture data + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded embedded metallic-roughness texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load embedded metallic-roughness texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the MaterialMesh to use the external texture path instead of gltf_texture_X + materialMesh.metallicRoughnessTexturePath = texturePath; + std::cout << " Loaded external metallic-roughness texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external metallic-roughness texture: " << texturePath << std::endl; + } + } + } + } + } else { + // Look for external metallic-roughness texture files that match this specific material + std::string materialName = materialNames[materialIndex]; + for (const auto & image : gltfModel.images) { + if (!image.uri.empty()) { + std::string imageUri = image.uri; + if ((imageUri.find("Metallic") != std::string::npos || + imageUri.find("Roughness") != std::string::npos || + imageUri.find("Specular") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { + std::string texturePath = baseTexturePath + imageUri; + materialMesh.metallicRoughnessTexturePath = texturePath; + std::cout << " Found external metallic-roughness texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } } - else if (gltfMaterial.normalTexture.index >= 0) { - int texIndex = gltfMaterial.normalTexture.index; - materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); + + // Extract occlusion texture + if (gltfMaterial.occlusionTexture.index >= 0) { + int texIndex = gltfMaterial.occlusionTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.occlusionTexturePath = textureId; + + // Load texture data (embedded or external) + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + // Load embedded texture data + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded embedded occlusion texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load embedded occlusion texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the MaterialMesh to use the external texture path instead of gltf_texture_X + materialMesh.occlusionTexturePath = texturePath; + std::cout << " Loaded external occlusion texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external occlusion texture: " << texturePath << std::endl; + } + } + } + } + } else { + // Look for external occlusion texture files that match this specific material + std::string materialName = materialNames[materialIndex]; + for (const auto & image : gltfModel.images) { + if (!image.uri.empty()) { + std::string imageUri = image.uri; + if ((imageUri.find("Occlusion") != std::string::npos || + imageUri.find("AO") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { + std::string texturePath = baseTexturePath + imageUri; + materialMesh.occlusionTexturePath = texturePath; + std::cout << " Found external occlusion texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } } - } - std::cout << " Texture path: " << (materialMesh.texturePath.empty() ? "none" : materialMesh.texturePath) << std::endl; + // Extract emissive texture + if (gltfMaterial.emissiveTexture.index >= 0) { + int texIndex = gltfMaterial.emissiveTexture.index; + if (texIndex < gltfModel.textures.size()) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.emissiveTexturePath = textureId; + + // Load texture data (embedded or external) + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + // Load embedded texture data + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded embedded emissive texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load embedded emissive texture: " << textureId << std::endl; + } + } else if (!image.uri.empty()) { + // Load external texture file + std::string texturePath = baseTexturePath + image.uri; + if (renderer->LoadTexture(texturePath)) { + // Update the MaterialMesh to use the external texture path instead of gltf_texture_X + materialMesh.emissiveTexturePath = texturePath; + std::cout << " Loaded external emissive texture: " << texturePath << std::endl; + } else { + std::cerr << " Failed to load external emissive texture: " << texturePath << std::endl; + } + } + } + } + } else { + // Look for external emissive texture files that match this specific material + std::string materialName = materialNames[materialIndex]; + for (const auto & image : gltfModel.images) { + if (!image.uri.empty()) { + std::string imageUri = image.uri; + if ((imageUri.find("Emissive") != std::string::npos || + imageUri.find("Emission") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { + std::string texturePath = baseTexturePath + imageUri; + materialMesh.emissiveTexturePath = texturePath; + std::cout << " Found external emissive texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } + } + } modelMaterialMeshes.push_back(materialMesh); @@ -563,7 +950,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, const std::string& normalMap, const std::string& metallicRoughnessMap, const std::string& aoMap, - const std::string& emissiveMap) { + const std::string& emissiveMap) const { if (!material) { std::cerr << "ModelLoader::LoadPBRTextures: Material is null" << std::endl; return false; @@ -589,7 +976,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, } else { // Use shared default albedo texture (much more efficient than creating per-material textures) std::cout << " Using shared default albedo texture" << std::endl; - material->albedoTexturePath = renderer->SHARED_DEFAULT_ALBEDO_ID; + material->albedoTexturePath = Renderer::SHARED_DEFAULT_ALBEDO_ID; } // Load normal map or create default @@ -603,7 +990,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, } else { // Use shared default normal texture (much more efficient than creating per-material textures) std::cout << " Using shared default normal texture" << std::endl; - material->normalTexturePath = renderer->SHARED_DEFAULT_NORMAL_ID; + material->normalTexturePath = Renderer::SHARED_DEFAULT_NORMAL_ID; } // Load metallic-roughness map or create default @@ -617,7 +1004,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, } else { // Use shared default metallic-roughness texture (much more efficient than creating per-material textures) std::cout << " Using shared default metallic-roughness texture" << std::endl; - material->metallicRoughnessTexturePath = renderer->SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; + material->metallicRoughnessTexturePath = Renderer::SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; } // Load ambient occlusion map or create default @@ -631,7 +1018,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, } else { // Use shared default occlusion texture (much more efficient than creating per-material textures) std::cout << " Using shared default occlusion texture" << std::endl; - material->occlusionTexturePath = renderer->SHARED_DEFAULT_OCCLUSION_ID; + material->occlusionTexturePath = Renderer::SHARED_DEFAULT_OCCLUSION_ID; } // Load emissive map or create default @@ -645,7 +1032,7 @@ bool ModelLoader::LoadPBRTextures(Material* material, } else { // Use shared default emissive texture (much more efficient than creating per-material textures) std::cout << " Using shared default emissive texture" << std::endl; - material->emissiveTexturePath = renderer->SHARED_DEFAULT_EMISSIVE_ID; + material->emissiveTexturePath = Renderer::SHARED_DEFAULT_EMISSIVE_ID; } std::cout << "PBR texture paths stored for material: " << material->GetName() << std::endl; @@ -695,11 +1082,76 @@ std::string ModelLoader::GetFirstMaterialTexturePath(const std::string& modelNam } std::vector ModelLoader::GetExtractedLights(const std::string& modelName) const { - auto it = extractedLights.find(modelName); - if (it != extractedLights.end()) { - return it->second; + std::vector lights; + + // First, try to get punctual lights from the extracted lights storage + auto lightIt = extractedLights.find(modelName); + if (lightIt != extractedLights.end()) { + lights = lightIt->second; + std::cout << "Found " << lights.size() << " punctual lights for model: " << modelName << std::endl; } - return std::vector(); + + // Now extract emissive materials as light sources + auto materialMeshIt = materialMeshes.find(modelName); + if (materialMeshIt != materialMeshes.end()) { + for (const auto& materialMesh : materialMeshIt->second) { + // Get the material for this mesh + auto materialIt = materials.find(materialMesh.materialName); + if (materialIt != materials.end()) { + const Material* material = materialIt->second.get(); + + // Check if this material has emissive properties (no threshold filtering) + float emissiveIntensity = glm::length(material->emissive) * material->emissiveStrength; + if (emissiveIntensity >= 0.0f) { // Accept all emissive materials, including zero intensity + // Calculate the center position of the emissive surface + glm::vec3 center(0.0f); + if (!materialMesh.vertices.empty()) { + for (const auto& vertex : materialMesh.vertices) { + center += vertex.position; + } + center /= static_cast(materialMesh.vertices.size()); + } + + // Calculate a reasonable direction (average normal of the surface) + glm::vec3 avgNormal(0.0f); + if (!materialMesh.vertices.empty()) { + for (const auto& vertex : materialMesh.vertices) { + avgNormal += vertex.normal; + } + avgNormal = glm::normalize(avgNormal / static_cast(materialMesh.vertices.size())); + } else { + avgNormal = glm::vec3(0.0f, -1.0f, 0.0f); // Default downward direction + } + + // CRITICAL FIX: Offset the light position away from the surface + // This allows the emissive light to properly illuminate the surface from outside + float offsetDistance = 0.5f; // Offset distance from surface + glm::vec3 lightPosition = center + avgNormal * offsetDistance; + + // Create an emissive light source + ExtractedLight emissiveLight; + emissiveLight.type = ExtractedLight::Type::Emissive; + emissiveLight.position = lightPosition; // Use offset position + emissiveLight.color = material->emissive; + emissiveLight.intensity = material->emissiveStrength; + emissiveLight.range = 10.0f; // Default range for emissive lights + emissiveLight.sourceMaterial = material->GetName(); + emissiveLight.direction = avgNormal; + + lights.push_back(emissiveLight); + + std::cout << "Created emissive light from material '" << material->GetName() + << "' at position (" << center.x << ", " << center.y << ", " << center.z + << ") with intensity " << emissiveIntensity << std::endl; + } + } + } + } + + std::cout << "Total lights extracted for model '" << modelName << "': " << lights.size() + << " (including emissive-derived lights)" << std::endl; + + return lights; } const std::vector& ModelLoader::GetMaterialMeshes(const std::string& modelName) const { @@ -708,7 +1160,7 @@ const std::vector& ModelLoader::GetMaterialMeshes(const std::strin return it->second; } // Return a static empty vector to avoid creating temporary objects - static const std::vector emptyVector; + static constexpr std::vector emptyVector; return emptyVector; } @@ -723,43 +1175,170 @@ Material* ModelLoader::GetMaterial(const std::string& materialName) const { bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const std::string& modelName) { std::cout << "Extracting punctual lights from model: " << modelName << std::endl; + std::vector lights; + // Check if the model has the KHR_lights_punctual extension auto extensionIt = gltfModel.extensions.find("KHR_lights_punctual"); - if (extensionIt == gltfModel.extensions.end()) { + if (extensionIt != gltfModel.extensions.end()) { + std::cout << " Found KHR_lights_punctual extension" << std::endl; + + // Parse the punctual lights from the extension + const tinygltf::Value& extension = extensionIt->second; + if (extension.Has("lights") && extension.Get("lights").IsArray()) { + const tinygltf::Value::Array& lightsArray = extension.Get("lights").Get(); + + for (size_t i = 0; i < lightsArray.size(); ++i) { + const tinygltf::Value& lightValue = lightsArray[i]; + if (!lightValue.IsObject()) continue; + + ExtractedLight light; + + // Parse light type + if (lightValue.Has("type") && lightValue.Get("type").IsString()) { + std::string type = lightValue.Get("type").Get(); + if (type == "directional") { + light.type = ExtractedLight::Type::Directional; + } else if (type == "point") { + light.type = ExtractedLight::Type::Point; + } else if (type == "spot") { + light.type = ExtractedLight::Type::Spot; + } + } + + // Parse light color + if (lightValue.Has("color") && lightValue.Get("color").IsArray()) { + const tinygltf::Value::Array& colorArray = lightValue.Get("color").Get(); + if (colorArray.size() >= 3) { + light.color = glm::vec3( + colorArray[0].IsNumber() ? static_cast(colorArray[0].Get()) : 1.0f, + colorArray[1].IsNumber() ? static_cast(colorArray[1].Get()) : 1.0f, + colorArray[2].IsNumber() ? static_cast(colorArray[2].Get()) : 1.0f + ); + } + } + + // Parse light intensity + if (lightValue.Has("intensity") && lightValue.Get("intensity").IsNumber()) { + light.intensity = static_cast(lightValue.Get("intensity").Get()); + } + + // Parse light range (for point and spot lights) + if (lightValue.Has("range") && lightValue.Get("range").IsNumber()) { + light.range = static_cast(lightValue.Get("range").Get()); + } + + // Parse spot light specific parameters + if (light.type == ExtractedLight::Type::Spot && lightValue.Has("spot")) { + const tinygltf::Value& spotValue = lightValue.Get("spot"); + if (spotValue.Has("innerConeAngle") && spotValue.Get("innerConeAngle").IsNumber()) { + light.innerConeAngle = static_cast(spotValue.Get("innerConeAngle").Get()); + } + if (spotValue.Has("outerConeAngle") && spotValue.Get("outerConeAngle").IsNumber()) { + light.outerConeAngle = static_cast(spotValue.Get("outerConeAngle").Get()); + } + } + + lights.push_back(light); + std::cout << " Parsed punctual light " << i << ": type=" << static_cast(light.type) + << ", intensity=" << light.intensity << std::endl; + } + } + } else { std::cout << " No KHR_lights_punctual extension found" << std::endl; - return true; // Not an error, just no punctual lights } - std::cout << " Found KHR_lights_punctual extension" << std::endl; + // Now find light nodes in the scene to get positions and directions + for (const auto& node : gltfModel.nodes) { + if (node.extensions.contains("KHR_lights_punctual")) { + const tinygltf::Value& nodeExtension = node.extensions.at("KHR_lights_punctual"); + if (nodeExtension.Has("light") && nodeExtension.Get("light").IsInt()) { + int lightIndex = nodeExtension.Get("light").Get(); + if (lightIndex >= 0 && lightIndex < static_cast(lights.size())) { + // Extract position from node transform + if (node.translation.size() >= 3) { + lights[lightIndex].position = glm::vec3( + static_cast(node.translation[0]), + static_cast(node.translation[1]), + static_cast(node.translation[2]) + ); + } - // TODO: Parse the punctual lights from the extension - // This would require parsing the JSON structure of the extension - // For now, we'll focus on emissive lights + // Extract direction from node rotation (for directional and spot lights) + if (node.rotation.size() >= 4 && + (lights[lightIndex].type == ExtractedLight::Type::Directional || + lights[lightIndex].type == ExtractedLight::Type::Spot)) { + // Convert quaternion to direction vector + glm::quat rotation( + static_cast(node.rotation[3]), // w + static_cast(node.rotation[0]), // x + static_cast(node.rotation[1]), // y + static_cast(node.rotation[2]) // z + ); + // Default forward direction in glTF is -Z + lights[lightIndex].direction = rotation * glm::vec3(0.0f, 0.0f, -1.0f); + } - return true; + std::cout << " Light " << lightIndex << " positioned at (" + << lights[lightIndex].position.x << ", " + << lights[lightIndex].position.y << ", " + << lights[lightIndex].position.z << ")" << std::endl; + } + } + } + } + + // Store the extracted lights + extractedLights[modelName] = lights; + + std::cout << " Extracted " << lights.size() << " total lights from model" << std::endl; + return lights.empty(); } -std::vector ModelLoader::ParseGLTFDataOnly(const std::string& filename) { - std::cout << "Thread-safe parsing GLTF file: " << filename << std::endl; +bool ModelLoader::LoadEmbeddedGLTFTextures(const std::string& filename) const { + std::cout << "Loading embedded GLTF textures from: " << filename << std::endl; - // Create tinygltf loader + if (!renderer) { + std::cerr << "LoadEmbeddedGLTFTextures: Renderer is null" << std::endl; + return false; + } + + // Create a tinygltf loader with proper image loading tinygltf::Model gltfModel; tinygltf::TinyGLTF loader; std::string err; std::string warn; - // Set up a dummy image loader callback that doesn't load actual image data + // Set up a proper image loader callback using stb_image (same as ParseGLTF) loader.SetImageLoader([](tinygltf::Image* image, const int image_idx, std::string* err, std::string* warn, int req_width, int req_height, const unsigned char* bytes, int size, void* user_data) -> bool { - // Just set basic image properties without loading actual data - // This avoids any potential file I/O or memory allocation issues in background thread - image->width = 1; - image->height = 1; - image->component = 4; // RGBA + // Use stb_image to decode the image data + int width, height, channels; + unsigned char* data = stbi_load_from_memory(bytes, size, &width, &height, &channels, 0); + + if (!data) { + if (err) { + *err = "Failed to load image with stb_image: " + std::string(stbi_failure_reason()); + } + return false; + } + + // Set image properties + image->width = width; + image->height = height; + image->component = channels; image->bits = 8; image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; - image->image.resize(4, 255); // Dummy white pixel + + // Copy image data + size_t image_size = width * height * channels; + image->image.resize(image_size); + std::memcpy(image->image.data(), data, image_size); + + // Free stb_image data + stbi_image_free(data); + + std::cout << " Loaded embedded texture: " << width << "x" << height << " with " << channels << " channels" << std::endl; return true; }, nullptr); @@ -777,190 +1356,39 @@ std::vector ModelLoader::ParseGLTFDataOnly(const std::string& file if (!err.empty()) { std::cerr << "GLTF Error: " << err << std::endl; - return std::vector(); + return false; } if (!ret) { - std::cerr << "Failed to parse GLTF file: " << filename << std::endl; - return std::vector(); - } - - std::cout << "Successfully loaded GLTF file with " << gltfModel.meshes.size() << " meshes (thread-safe)" << std::endl; - - // Extract mesh data from the first mesh (for now, we'll handle multiple meshes later) - if (gltfModel.meshes.empty()) { - std::cerr << "No meshes found in GLTF file" << std::endl; - return std::vector(); + std::cerr << "Failed to parse GLTF file for texture loading: " << filename << std::endl; + return false; } - // Group primitives by material to create separate meshes for each material - std::map> materialVertices; - std::map> materialIndices; - std::map materialNames; - - // Process all meshes and group by material - for (const auto& mesh : gltfModel.meshes) { - std::cout << "Processing mesh: " << mesh.name << " (thread-safe)" << std::endl; - - for (const auto& primitive : mesh.primitives) { - // Get the material index for this primitive - int materialIndex = primitive.material; - if (materialIndex < 0) { - materialIndex = -1; // Use -1 for primitives without materials - } - - // Initialize vectors for this material if not already done - if (materialVertices.find(materialIndex) == materialVertices.end()) { - materialVertices[materialIndex] = std::vector(); - materialIndices[materialIndex] = std::vector(); - - // Store material name for debugging - if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { - const auto& gltfMaterial = gltfModel.materials[materialIndex]; - materialNames[materialIndex] = gltfMaterial.name.empty() ? - ("material_" + std::to_string(materialIndex)) : gltfMaterial.name; + std::cout << "Successfully loaded GLTF file for texture extraction" << std::endl; + + // Load all embedded textures using LoadTextureFromMemory + int texturesLoaded = 0; + for (size_t texIndex = 0; texIndex < gltfModel.textures.size(); ++texIndex) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + const auto& image = gltfModel.images[texture.source]; + + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + std::cout << " Loaded embedded texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + texturesLoaded++; } else { - materialNames[materialIndex] = "no_material"; - } - - std::cout << " Found material " << materialIndex << ": " << materialNames[materialIndex] << " (thread-safe)" << std::endl; - } - - // Get indices for this material - if (primitive.indices >= 0) { - const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; - const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; - const tinygltf::Buffer& indexBuffer = gltfModel.buffers[indexBufferView.buffer]; - - const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; - - size_t indexOffset = materialVertices[materialIndex].size(); - - // Handle different index types - if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - const uint16_t* buf = static_cast(indexData); - for (size_t i = 0; i < indexAccessor.count; ++i) { - materialIndices[materialIndex].push_back(buf[i] + indexOffset); - } - } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - const uint32_t* buf = static_cast(indexData); - for (size_t i = 0; i < indexAccessor.count; ++i) { - materialIndices[materialIndex].push_back(buf[i] + indexOffset); - } + std::cerr << " Failed to load embedded texture: " << textureId << std::endl; } - } - - // Get vertex positions - auto posIt = primitive.attributes.find("POSITION"); - if (posIt == primitive.attributes.end()) { - std::cerr << "No POSITION attribute found in primitive" << std::endl; - continue; - } - - const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; - const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; - const tinygltf::Buffer& posBuffer = gltfModel.buffers[posBufferView.buffer]; - - const float* positions = reinterpret_cast( - &posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset]); - - // Get texture coordinates (if available) - const float* texCoords = nullptr; - auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); - if (texCoordIt != primitive.attributes.end()) { - const tinygltf::Accessor& texCoordAccessor = gltfModel.accessors[texCoordIt->second]; - const tinygltf::BufferView& texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; - const tinygltf::Buffer& texCoordBuffer = gltfModel.buffers[texCoordBufferView.buffer]; - texCoords = reinterpret_cast( - &texCoordBuffer.data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]); - } - - // Get normals (if available) - const float* normals = nullptr; - auto normalIt = primitive.attributes.find("NORMAL"); - if (normalIt != primitive.attributes.end()) { - const tinygltf::Accessor& normalAccessor = gltfModel.accessors[normalIt->second]; - const tinygltf::BufferView& normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; - const tinygltf::Buffer& normalBuffer = gltfModel.buffers[normalBufferView.buffer]; - normals = reinterpret_cast( - &normalBuffer.data[normalBufferView.byteOffset + normalAccessor.byteOffset]); - } - - // Create vertices for this material - for (size_t i = 0; i < posAccessor.count; ++i) { - Vertex vertex{}; - - // Position - vertex.position = glm::vec3( - positions[i * 3 + 0], - positions[i * 3 + 1], - positions[i * 3 + 2] - ); - - // Normal (use extracted normals if available, otherwise default up) - if (normals) { - vertex.normal = glm::vec3( - normals[i * 3 + 0], - normals[i * 3 + 1], - normals[i * 3 + 2] - ); - } else { - vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); // Default forward normal - } - - // Texture coordinates - if (texCoords) { - vertex.texCoord = glm::vec2( - texCoords[i * 2 + 0], - texCoords[i * 2 + 1] - ); - } else { - vertex.texCoord = glm::vec2(0.0f, 0.0f); - } - - // Tangent (default right tangent for now, could be extracted from GLTF if available) - vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - - materialVertices[materialIndex].push_back(vertex); + } else { + std::cerr << " Empty image data for texture: " << textureId << std::endl; } } } - // Create material meshes with texture path information (but don't load textures) - std::vector modelMaterialMeshes; - - std::cout << "Processing " << materialVertices.size() << " materials (thread-safe):" << std::endl; - - for (const auto& materialPair : materialVertices) { - int materialIndex = materialPair.first; - const auto& vertices = materialPair.second; - const auto& indices = materialIndices[materialIndex]; - - std::cout << " Material " << materialIndex << " (" << materialNames[materialIndex] - << "): " << vertices.size() << " vertices, " << indices.size() << " indices (thread-safe)" << std::endl; - - // Create MaterialMesh for this material - MaterialMesh materialMesh; - materialMesh.materialIndex = materialIndex; - materialMesh.materialName = materialNames[materialIndex]; - materialMesh.vertices = vertices; - materialMesh.indices = indices; - - // Get a texture path for this material (but don't load the texture) - if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { - const auto& gltfMaterial = gltfModel.materials[materialIndex]; - - if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { - int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; - materialMesh.texturePath = "gltf_texture_" + std::to_string(texIndex); - } - } - - std::cout << " Texture path: " << (materialMesh.texturePath.empty() ? "none" : materialMesh.texturePath) << " (thread-safe)" << std::endl; - - modelMaterialMeshes.push_back(materialMesh); - } - - std::cout << "Thread-safe GLTF parsing completed with " << modelMaterialMeshes.size() << " material meshes" << std::endl; - return modelMaterialMeshes; + std::cout << "Successfully loaded " << texturesLoaded << " embedded GLTF textures" << std::endl; + return texturesLoaded > 0; } diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index 2b5e95f3..ce3413ff 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include #include #include +#include #include "mesh_component.h" class Renderer; @@ -18,10 +20,10 @@ namespace tinygltf { class Material { public: - Material(const std::string& name) : name(name) {} + explicit Material(std::string name) : name(std::move(name)) {} ~Material() = default; - const std::string& GetName() const { return name; } + [[nodiscard]] const std::string& GetName() const { return name; } // PBR properties glm::vec3 albedo = glm::vec3(1.0f); @@ -29,6 +31,7 @@ class Material { float roughness = 1.0f; float ao = 1.0f; glm::vec3 emissive = glm::vec3(0.0f); + float emissiveStrength = 1.0f; // KHR_materials_emissive_strength extension // Texture paths for PBR materials std::string albedoTexturePath; @@ -64,6 +67,29 @@ struct ExtractedLight { std::string sourceMaterial; // Name of source material (for emissive lights) }; +/** + * @brief Structure representing camera data extracted from GLTF. + */ +struct CameraData { + std::string name; + bool isPerspective = true; + + // Perspective camera properties + float fov = 0.785398f; // 45 degrees in radians + float aspectRatio = 1.0f; + + // Orthographic camera properties + float orthographicSize = 1.0f; + + // Common properties + float nearPlane = 0.1f; + float farPlane = 1000.0f; + + // Transform properties + glm::vec3 position = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion +}; + /** * @brief Structure representing mesh data for a specific material. */ @@ -72,7 +98,14 @@ struct MaterialMesh { std::string materialName; std::vector vertices; std::vector indices; - std::string texturePath; // Primary texture path for this material + + // All PBR texture paths for this material + std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility + std::string baseColorTexturePath; // Base color (albedo) texture + std::string normalTexturePath; // Normal map texture + std::string metallicRoughnessTexturePath; // Metallic-roughness texture + std::string occlusionTexturePath; // Ambient occlusion texture + std::string emissiveTexturePath; // Emissive texture }; /** @@ -80,19 +113,26 @@ struct MaterialMesh { */ class Model { public: - Model(const std::string& name) : name(name) {} + explicit Model(std::string name) : name(std::move(name)) {} ~Model() = default; - const std::string& GetName() const { return name; } + [[nodiscard]] const std::string& GetName() const { return name; } // Mesh data access methods - const std::vector& GetVertices() const { return vertices; } - const std::vector& GetIndices() const { return indices; } + [[nodiscard]] const std::vector& GetVertices() const { return vertices; } + [[nodiscard]] const std::vector& GetIndices() const { return indices; } // Methods to set mesh data (used by parser) void SetVertices(const std::vector& newVertices) { vertices = newVertices; } void SetIndices(const std::vector& newIndices) { indices = newIndices; } + // Camera data access methods + [[nodiscard]] const std::vector& GetCameras() const { return cameras; } + +public: + // Public access to cameras for model loader + std::vector cameras; + private: std::string name; std::vector vertices; @@ -108,7 +148,7 @@ class ModelLoader { /** * @brief Default constructor. */ - ModelLoader(); + ModelLoader() = default; /** * @brief Destructor for proper cleanup. @@ -199,11 +239,11 @@ class ModelLoader { Material* GetMaterial(const std::string& materialName) const; /** - * @brief Parse GLTF file data without creating Vulkan resources (thread-safe). + * @brief Load embedded GLTF textures. * @param filename The path to the GLTF file. - * @return Vector of material meshes with raw data only. + * @return True if textures were loaded successfully, false otherwise. */ - std::vector ParseGLTFDataOnly(const std::string& filename); + bool LoadEmbeddedGLTFTextures(const std::string& filename) const; private: // Reference to the renderer @@ -244,7 +284,7 @@ class ModelLoader { const std::string& normalMap, const std::string& metallicRoughnessMap, const std::string& aoMap, - const std::string& emissiveMap); + const std::string& emissiveMap) const; /** * @brief Extract lights from GLTF punctual lights extension. diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index c9b33e35..61cd78f6 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -79,19 +79,19 @@ class ConcreteRigidBody : public RigidBody { std::cout << "Setting rigid body angular velocity to (" << velocity.x << ", " << velocity.y << ", " << velocity.z << ")" << std::endl; } - glm::vec3 GetPosition() const override { + [[nodiscard]] glm::vec3 GetPosition() const override { return position; } - glm::quat GetRotation() const override { + [[nodiscard]] glm::quat GetRotation() const override { return rotation; } - glm::vec3 GetLinearVelocity() const override { + [[nodiscard]] glm::vec3 GetLinearVelocity() const override { return linearVelocity; } - glm::vec3 GetAngularVelocity() const override { + [[nodiscard]] glm::vec3 GetAngularVelocity() const override { return angularVelocity; } @@ -100,35 +100,35 @@ class ConcreteRigidBody : public RigidBody { std::cout << "Setting rigid body kinematic to " << (kinematic ? "true" : "false") << std::endl; } - bool IsKinematic() const override { + [[nodiscard]] bool IsKinematic() const override { return kinematic; } - Entity* GetEntity() const { + [[nodiscard]] Entity* GetEntity() const { return entity; } - CollisionShape GetShape() const { + [[nodiscard]] CollisionShape GetShape() const { return shape; } - float GetMass() const { + [[nodiscard]] float GetMass() const { return mass; } - float GetInverseMass() const { + [[nodiscard]] float GetInverseMass() const { return mass > 0.0f ? 1.0f / mass : 0.0f; } - float GetRestitution() const { + [[nodiscard]] float GetRestitution() const { return restitution; } - float GetFriction() const { + [[nodiscard]] float GetFriction() const { return friction; } - void Update(float deltaTime, const glm::vec3& gravity) { + void Update(const float deltaTime, const glm::vec3& gravity) { if (kinematic) { return; } @@ -140,7 +140,7 @@ class ConcreteRigidBody : public RigidBody { position += linearVelocity * deltaTime; // Update rotation - glm::quat angularVelocityQuat(0.0f, angularVelocity.x, angularVelocity.y, angularVelocity.z); + const glm::quat angularVelocityQuat(0.0f, angularVelocity.x, angularVelocity.y, angularVelocity.z); rotation += 0.5f * deltaTime * angularVelocityQuat * rotation; rotation = glm::normalize(rotation); @@ -170,10 +170,6 @@ class ConcreteRigidBody : public RigidBody { friend class PhysicsSystem; }; -PhysicsSystem::PhysicsSystem() { - // Constructor implementation -} - PhysicsSystem::~PhysicsSystem() { // Destructor implementation if (initialized && gpuAccelerationEnabled) { @@ -188,7 +184,7 @@ bool PhysicsSystem::Initialize() { std::cout << "Initializing physics system" << std::endl; - // Initialize Vulkan resources if GPU acceleration is enabled and renderer is set + // Initialize Vulkan resources if GPU acceleration is enabled and the renderer is set if (gpuAccelerationEnabled && renderer) { if (!InitializeVulkanResources()) { std::cerr << "Failed to initialize Vulkan resources for physics system" << std::endl; @@ -201,14 +197,14 @@ bool PhysicsSystem::Initialize() { } void PhysicsSystem::Update(float deltaTime) { - // If GPU acceleration is enabled and we have a renderer, use the GPU + // If GPU acceleration is enabled, and we have a renderer, use the GPU if (initialized && gpuAccelerationEnabled && renderer && rigidBodies.size() <= maxGPUObjects) { SimulatePhysicsOnGPU(deltaTime); } else { // Fall back to CPU physics // Update all rigid bodies for (auto& rigidBody : rigidBodies) { - auto concreteRigidBody = static_cast(rigidBody.get()); + auto concreteRigidBody = dynamic_cast(rigidBody.get()); concreteRigidBody->Update(deltaTime, gravity); } @@ -222,19 +218,18 @@ RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, auto rigidBody = std::make_unique(entity, shape, mass); // Store the rigid body - RigidBody* rigidBodyPtr = rigidBody.get(); rigidBodies.push_back(std::move(rigidBody)); std::cout << "Rigid body created for entity: " << (entity ? entity->GetName() : "null") << std::endl; - return rigidBodyPtr; + return rigidBodies.back().get(); } bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { // Find the rigid body in the vector - auto it = std::find_if(rigidBodies.begin(), rigidBodies.end(), - [rigidBody](const std::unique_ptr& rb) { - return rb.get() == rigidBody; - }); + auto it = std::ranges::find_if(rigidBodies, + [rigidBody](const std::unique_ptr& rb) { + return rb.get() == rigidBody; + }); if (it != rigidBodies.end()) { // Remove the rigid body @@ -259,11 +254,7 @@ glm::vec3 PhysicsSystem::GetGravity() const { } bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, - glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity) { - std::cout << "Performing raycast from (" << origin.x << ", " << origin.y << ", " << origin.z << ") " - << "in direction (" << direction.x << ", " << direction.y << ", " << direction.z << ") " - << "with max distance " << maxDistance << std::endl; - + glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity) const { // Normalize the direction vector glm::vec3 normalizedDirection = glm::normalize(direction); @@ -276,7 +267,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, // Check each rigid body for intersection for (const auto& rigidBody : rigidBodies) { - auto concreteRigidBody = static_cast(rigidBody.get()); + auto concreteRigidBody = dynamic_cast(rigidBody.get()); Entity* entity = concreteRigidBody->GetEntity(); // Skip if the entity is null @@ -311,7 +302,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, // Calculate intersection distance float t = (-b - std::sqrt(discriminant)) / (2.0f * a); - // Check if intersection is within range + // Check if the intersection is within range if (t > 0 && t < closestHitDistance) { hitDistance = t; localHitPosition = origin + normalizedDirection * t; @@ -363,7 +354,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, } } - // Check if intersection is within range + // Check if the intersection is within range if (tmin > 0 && tmin < closestHitDistance) { hitDistance = tmin; localHitPosition = origin + normalizedDirection * tmin; @@ -395,7 +386,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, glm::vec3 capsuleA = position + glm::vec3(0, -halfHeight, 0); glm::vec3 capsuleB = position + glm::vec3(0, halfHeight, 0); - // Calculate closest point on line segment + // Calculate the closest point on a line segment glm::vec3 ab = capsuleB - capsuleA; glm::vec3 ao = origin - capsuleA; @@ -404,21 +395,19 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, glm::vec3 closestPoint = capsuleA + ab * t; - // Sphere intersection test with closest point + // Sphere intersection test with the closest point glm::vec3 oc = origin - closestPoint; float a = glm::dot(normalizedDirection, normalizedDirection); float b = 2.0f * glm::dot(oc, normalizedDirection); float c = glm::dot(oc, oc) - radius * radius; - float discriminant = b * b - 4 * a * c; - if (discriminant >= 0) { + if (float discriminant = b * b - 4 * a * c; discriminant >= 0) { // Calculate intersection distance - float t = (-b - std::sqrt(discriminant)) / (2.0f * a); - // Check if intersection is within range - if (t > 0 && t < closestHitDistance) { - hitDistance = t; - localHitPosition = origin + normalizedDirection * t; + // Check if the intersection is within range + if (float id = (-b - std::sqrt(discriminant)) / (2.0f * a); id > 0 && id < closestHitDistance) { + hitDistance = id; + localHitPosition = origin + normalizedDirection * id; localHitNormal = glm::normalize(localHitPosition - closestPoint); hit = true; } @@ -443,7 +432,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, // Calculate intersection distance float t = (-b - std::sqrt(discriminant)) / (2.0f * a); - // Check if intersection is within range + // Check if the intersection is within range if (t > 0 && t < closestHitDistance) { hitDistance = t; localHitPosition = origin + normalizedDirection * t; @@ -457,7 +446,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, break; } - // Update closest hit if a hit was found + // Update the closest hit if a hit was found if (hit && hitDistance < closestHitDistance) { closestHitDistance = hitDistance; closestHitPosition = localHitPosition; @@ -497,11 +486,11 @@ static std::vector readFile(const std::string& filename) { throw std::runtime_error("Failed to open file: " + filename); } - size_t fileSize = (size_t)file.tellg(); + size_t fileSize = file.tellg(); std::vector buffer(fileSize); file.seekg(0); - file.read(buffer.data(), fileSize); + file.read(buffer.data(), static_cast(fileSize)); file.close(); return buffer; @@ -514,7 +503,7 @@ static vk::raii::ShaderModule createShaderModule(const vk::raii::Device& device, createInfo.pCode = reinterpret_cast(code.data()); try { - return vk::raii::ShaderModule(device, createInfo); + return {device, createInfo}; } catch (const std::exception& e) { throw std::runtime_error("Failed to create shader module: " + std::string(e.what())); } @@ -595,24 +584,14 @@ bool PhysicsSystem::InitializeVulkanResources() { vk::DescriptorSetLayoutCreateInfo layoutInfo; layoutInfo.bindingCount = static_cast(bindings.size()); layoutInfo.pBindings = bindings.data(); - - try { - vulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(raiiDevice, layoutInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create descriptor set layout: " + std::string(e.what())); - } + vulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(raiiDevice, layoutInfo); // Create pipeline layout vk::PipelineLayoutCreateInfo pipelineLayoutInfo; pipelineLayoutInfo.setLayoutCount = 1; vk::DescriptorSetLayout descriptorSetLayout = *vulkanResources.descriptorSetLayout; pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; - - try { - vulkanResources.pipelineLayout = vk::raii::PipelineLayout(raiiDevice, pipelineLayoutInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create pipeline layout: " + std::string(e.what())); - } + vulkanResources.pipelineLayout = vk::raii::PipelineLayout(raiiDevice, pipelineLayoutInfo); // Create compute pipelines vk::ComputePipelineCreateInfo pipelineInfo; @@ -625,12 +604,7 @@ bool PhysicsSystem::InitializeVulkanResources() { integrateStageInfo.module = *vulkanResources.integrateShaderModule; integrateStageInfo.pName = "IntegrateCS"; pipelineInfo.stage = integrateStageInfo; - - try { - vulkanResources.integratePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create integrate compute pipeline: " + std::string(e.what())); - } + vulkanResources.integratePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); // Broad phase pipeline vk::PipelineShaderStageCreateInfo broadPhaseStageInfo; @@ -638,12 +612,7 @@ bool PhysicsSystem::InitializeVulkanResources() { broadPhaseStageInfo.module = *vulkanResources.broadPhaseShaderModule; broadPhaseStageInfo.pName = "BroadPhaseCS"; pipelineInfo.stage = broadPhaseStageInfo; - - try { - vulkanResources.broadPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create broad phase compute pipeline: " + std::string(e.what())); - } + vulkanResources.broadPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); // Narrow phase pipeline vk::PipelineShaderStageCreateInfo narrowPhaseStageInfo; @@ -651,12 +620,7 @@ bool PhysicsSystem::InitializeVulkanResources() { narrowPhaseStageInfo.module = *vulkanResources.narrowPhaseShaderModule; narrowPhaseStageInfo.pName = "NarrowPhaseCS"; pipelineInfo.stage = narrowPhaseStageInfo; - - try { - vulkanResources.narrowPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create narrow phase compute pipeline: " + std::string(e.what())); - } + vulkanResources.narrowPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); // Resolve pipeline vk::PipelineShaderStageCreateInfo resolveStageInfo; @@ -664,12 +628,7 @@ bool PhysicsSystem::InitializeVulkanResources() { resolveStageInfo.module = *vulkanResources.resolveShaderModule; resolveStageInfo.pName = "ResolveCS"; pipelineInfo.stage = resolveStageInfo; - - try { - vulkanResources.resolvePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create resolve compute pipeline: " + std::string(e.what())); - } + vulkanResources.resolvePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); // Create buffers vk::DeviceSize physicsBufferSize = sizeof(GPUPhysicsData) * maxGPUObjects; @@ -678,7 +637,7 @@ bool PhysicsSystem::InitializeVulkanResources() { vk::DeviceSize counterBufferSize = sizeof(uint32_t) * 2; vk::DeviceSize paramsBufferSize = sizeof(PhysicsParams); - // Create physics buffer + // Create a physics buffer vk::BufferCreateInfo bufferInfo; bufferInfo.size = physicsBufferSize; bufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; @@ -702,7 +661,7 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create physics buffer: " + std::string(e.what())); } - // Create collision buffer + // Create a collision buffer bufferInfo.size = collisionBufferSize; try { vulkanResources.collisionBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); @@ -722,7 +681,7 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create collision buffer: " + std::string(e.what())); } - // Create pair buffer + // Create a pair buffer bufferInfo.size = pairBufferSize; try { vulkanResources.pairBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); @@ -742,7 +701,7 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create pair buffer: " + std::string(e.what())); } - // Create counter buffer + // Create counter-buffer bufferInfo.size = counterBufferSize; try { vulkanResources.counterBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); @@ -762,7 +721,7 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create counter buffer: " + std::string(e.what())); } - // Create params buffer + // Create a params buffer bufferInfo.size = paramsBufferSize; bufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; try { @@ -783,14 +742,14 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create params buffer: " + std::string(e.what())); } - // Initialize counter buffer + // Initialize counter-buffer uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count void* data = vulkanResources.counterBufferMemory.mapMemory(0, sizeof(initialCounters)); memcpy(data, initialCounters, sizeof(initialCounters)); vulkanResources.counterBufferMemory.unmapMemory(); - // Create descriptor pool - std::array poolSizes = { + // Create a descriptor pool + std::array poolSizes = { vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 4), vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1) }; @@ -799,12 +758,7 @@ bool PhysicsSystem::InitializeVulkanResources() { poolInfo.poolSizeCount = static_cast(poolSizes.size()); poolInfo.pPoolSizes = poolSizes.data(); poolInfo.maxSets = 1; - - try { - vulkanResources.descriptorPool = vk::raii::DescriptorPool(raiiDevice, poolInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create descriptor pool: " + std::string(e.what())); - } + vulkanResources.descriptorPool = vk::raii::DescriptorPool(raiiDevice, poolInfo); // Allocate descriptor sets vk::DescriptorSetAllocateInfo descriptorSetAllocInfo; @@ -893,16 +847,11 @@ bool PhysicsSystem::InitializeVulkanResources() { raiiDevice.updateDescriptorSets(descriptorWrites, nullptr); - // Create command pool + // Create a command pool vk::CommandPoolCreateInfo commandPoolInfo; commandPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; - commandPoolInfo.queueFamilyIndex = 0; // Assuming compute queue family index is 0 - - try { - vulkanResources.commandPool = vk::raii::CommandPool(raiiDevice, commandPoolInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create command pool: " + std::string(e.what())); - } + commandPoolInfo.queueFamilyIndex = 0; // Assuming the compute queue family index is 0 + vulkanResources.commandPool = vk::raii::CommandPool(raiiDevice, commandPoolInfo); // Allocate command buffer vk::CommandBufferAllocateInfo commandBufferInfo; @@ -960,7 +909,7 @@ void PhysicsSystem::CleanupVulkanResources() { vulkanResources.integrateShaderModule = nullptr; } -void PhysicsSystem::UpdateGPUPhysicsData() { +void PhysicsSystem::UpdateGPUPhysicsData() const { if (!renderer) { return; } @@ -973,16 +922,13 @@ void PhysicsSystem::UpdateGPUPhysicsData() { return; } - const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); - - // Map the physics buffer void* data = vulkanResources.physicsBufferMemory.mapMemory(0, sizeof(GPUPhysicsData) * rigidBodies.size()); // Copy physics data to the buffer - GPUPhysicsData* gpuData = static_cast(data); + auto* gpuData = static_cast(data); for (size_t i = 0; i < rigidBodies.size(); i++) { - auto concreteRigidBody = static_cast(rigidBodies[i].get()); + const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, @@ -992,9 +938,8 @@ void PhysicsSystem::UpdateGPUPhysicsData() { gpuData[i].force = glm::vec4(glm::vec3(0.0f), concreteRigidBody->IsKinematic() ? 1.0f : 0.0f); gpuData[i].torque = glm::vec4(glm::vec3(0.0f), 1.0f); // Always use gravity - // Set collider data based on collider type - CollisionShape shape = concreteRigidBody->GetShape(); - switch (shape) { + // Set collider data based on a collider type + switch (concreteRigidBody->GetShape()) { case CollisionShape::Sphere: gpuData[i].colliderData = glm::vec4(0.5f, 0.0f, 0.0f, static_cast(0)); // 0 = Sphere gpuData[i].colliderData2 = glm::vec4(0.0f); @@ -1019,7 +964,7 @@ void PhysicsSystem::UpdateGPUPhysicsData() { vulkanResources.counterBufferMemory.unmapMemory(); // Update params buffer - PhysicsParams params; + PhysicsParams params{}; params.deltaTime = 1.0f / 60.0f; // Fixed time step params.gravity = gravity; params.numBodies = static_cast(rigidBodies.size()); @@ -1030,7 +975,7 @@ void PhysicsSystem::UpdateGPUPhysicsData() { vulkanResources.paramsBufferMemory.unmapMemory(); } -void PhysicsSystem::ReadbackGPUPhysicsData() { +void PhysicsSystem::ReadbackGPUPhysicsData() const { if (!renderer) { return; } @@ -1041,16 +986,13 @@ void PhysicsSystem::ReadbackGPUPhysicsData() { return; } - const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); - - // Map the physics buffer void* data = vulkanResources.physicsBufferMemory.mapMemory(0, sizeof(GPUPhysicsData) * rigidBodies.size()); // Copy physics data from the buffer - GPUPhysicsData* gpuData = static_cast(data); + const auto* gpuData = static_cast(data); for (size_t i = 0; i < rigidBodies.size(); i++) { - auto concreteRigidBody = static_cast(rigidBodies[i].get()); + const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); // Skip kinematic bodies if (concreteRigidBody->IsKinematic()) { @@ -1067,7 +1009,7 @@ void PhysicsSystem::ReadbackGPUPhysicsData() { vulkanResources.physicsBufferMemory.unmapMemory(); } -void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { +void PhysicsSystem::SimulatePhysicsOnGPU(float) const { if (!renderer) { return; } @@ -1081,13 +1023,10 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { return; } - const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); - - // Update physics data on the GPU UpdateGPUPhysicsData(); - // Reset command buffer before beginning (required for reuse) + // Reset the command buffer before beginning (required for reuse) vulkanResources.commandBuffer.reset(); // Begin command buffer @@ -1129,7 +1068,7 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { uint32_t numPairs = (rigidBodies.size() * (rigidBodies.size() - 1)) / 2; vulkanResources.commandBuffer.dispatch((numPairs + 63) / 64, 1, 1); - // Memory barrier to ensure broad phase is complete before narrow phase + // Memory barrier to ensure the broad phase is complete before the narrow phase vulkanResources.commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eComputeShader, @@ -1144,7 +1083,7 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { // We don't know how many pairs were generated, so we use a conservative estimate vulkanResources.commandBuffer.dispatch((maxGPUCollisions + 63) / 64, 1, 1); - // Memory barrier to ensure narrow phase is complete before resolution + // Memory barrier to ensure a narrow phase is complete before resolution vulkanResources.commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eComputeShader, diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index e8cb8c98..9a8b7ec2 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -1,12 +1,9 @@ #pragma once -#include #include #include -#include #include #include -#include class Entity; class Renderer; @@ -181,7 +178,7 @@ class PhysicsSystem { /** * @brief Default constructor. */ - PhysicsSystem(); + PhysicsSystem() = default; /** * @brief Destructor for proper cleanup. @@ -239,7 +236,7 @@ class PhysicsSystem { * @return True if the ray hit something, false otherwise. */ bool Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, - glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity); + glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity) const; /** * @brief Enable or disable GPU acceleration. @@ -325,11 +322,11 @@ class PhysicsSystem { void CleanupVulkanResources(); // Update physics data on the GPU - void UpdateGPUPhysicsData(); + void UpdateGPUPhysicsData() const; // Read back physics data from the GPU - void ReadbackGPUPhysicsData(); + void ReadbackGPUPhysicsData() const; // Perform GPU-accelerated physics simulation - void SimulatePhysicsOnGPU(float deltaTime); + void SimulatePhysicsOnGPU(float deltaTime) const; }; diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h index 00047dec..a2724f90 100644 --- a/attachments/simple_engine/pipeline.h +++ b/attachments/simple_engine/pipeline.h @@ -11,14 +11,22 @@ #include "swap_chain.h" /** - * @brief Structure for material properties. + * @brief Structure for PBR material properties. + * This structure must match the PushConstants structure in the PBR shader. */ struct MaterialProperties { - alignas(16) glm::vec4 ambientColor; - alignas(16) glm::vec4 diffuseColor; - alignas(16) glm::vec4 specularColor; - alignas(4) float shininess; - alignas(4) float padding[3]; // Padding to ensure alignment + alignas(16) glm::vec4 baseColorFactor; + alignas(4) float metallicFactor; + alignas(4) float roughnessFactor; + alignas(4) int baseColorTextureSet; + alignas(4) int physicalDescriptorTextureSet; + alignas(4) int normalTextureSet; + alignas(4) int occlusionTextureSet; + alignas(4) int emissiveTextureSet; + alignas(4) float alphaMask; + alignas(4) float alphaMaskCutoff; + alignas(16) glm::vec3 emissiveFactor; // Emissive factor for HDR emissive sources + alignas(4) float emissiveStrength; // KHR_materials_emissive_strength extension }; /** diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 0d463e65..3039e6ce 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -14,6 +14,7 @@ #include "mesh_component.h" #include "camera_component.h" #include "memory_pool.h" +#include "model_loader.h" // Forward declarations class ImGuiSystem; @@ -26,7 +27,7 @@ struct QueueFamilyIndices { std::optional presentFamily; std::optional computeFamily; - bool isComplete() const { + [[nodiscard]] bool isComplete() const { return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); } }; @@ -41,30 +42,53 @@ struct SwapChainSupportDetails { }; /** - * @brief Structure for uniform buffer object. + * @brief Structure for individual light data in storage buffer. + */ +struct LightData { + alignas(16) glm::vec4 position; // Light position (w component unused) + alignas(16) glm::vec4 color; // Light color and intensity + alignas(16) glm::mat4 lightSpaceMatrix; // Light space matrix for shadow mapping + alignas(4) int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive + alignas(4) float range; // Light range + alignas(4) float innerConeAngle; // For spot lights + alignas(4) float outerConeAngle; // For spot lights +}; + +/** + * @brief Structure for uniform buffer object (now without fixed light arrays). */ struct UniformBufferObject { alignas(16) glm::mat4 model; alignas(16) glm::mat4 view; alignas(16) glm::mat4 proj; - alignas(16) glm::vec4 lightPositions[4]; - alignas(16) glm::vec4 lightColors[4]; alignas(16) glm::vec4 camPos; alignas(4) float exposure; alignas(4) float gamma; alignas(4) float prefilteredCubeMipLevels; alignas(4) float scaleIBLAmbient; + alignas(4) int lightCount; // Number of active lights (dynamic) + alignas(4) int shadowMapCount; // Number of active shadow maps (dynamic) + alignas(4) float shadowBias; // Shadow bias to prevent shadow acne + alignas(4) float padding; // Padding for alignment }; /** - * @brief Structure for material properties. + * @brief Structure for PBR material properties. + * This structure must match the PushConstants structure in the PBR shader. */ struct MaterialProperties { - alignas(16) glm::vec4 ambientColor; - alignas(16) glm::vec4 diffuseColor; - alignas(16) glm::vec4 specularColor; - alignas(4) float shininess; - alignas(4) float padding[3]; // Padding to ensure alignment + alignas(16) glm::vec4 baseColorFactor; + alignas(4) float metallicFactor; + alignas(4) float roughnessFactor; + alignas(4) int baseColorTextureSet; + alignas(4) int physicalDescriptorTextureSet; + alignas(4) int normalTextureSet; + alignas(4) int occlusionTextureSet; + alignas(4) int emissiveTextureSet; + alignas(4) float alphaMask; + alignas(4) float alphaMaskCutoff; + alignas(16) glm::vec3 emissiveFactor; // Emissive factor for HDR emissive sources + alignas(4) float emissiveStrength; // KHR_materials_emissive_strength extension }; /** @@ -247,7 +271,21 @@ class Renderer { * @param height The image height. */ void CopyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { - copyBufferToImage(buffer, image, width, height); + // Create default single region for backward compatibility + std::vector regions = {{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + }}; + copyBufferToImage(buffer, image, width, height, regions); } /** @@ -281,6 +319,73 @@ class Renderer { void SetModelLoader(class ModelLoader* modelLoader) { this->modelLoader = modelLoader; } + + /** + * @brief Set static lights loaded during model initialization. + * @param lights The lights to store statically. + */ + void SetStaticLights(const std::vector& lights) { staticLights = lights; } + + /** + * @brief Set the gamma correction value for PBR rendering. + * @param gamma The gamma correction value (typically 2.2). + */ + void SetGamma(float gamma) { + this->gamma = gamma; + } + + /** + * @brief Set the exposure value for HDR tone mapping. + * @param exposure The exposure value (1.0 = no adjustment). + */ + void SetExposure(float exposure) { + this->exposure = exposure; + } + + /** + * @brief Enable or disable shadow rendering. + * @param enabled True to enable shadows, false to disable. + * @note Shadows should be pre-computed and cached during model loading for optimal performance. + * Toggling this flag should only affect shader uniform values, not trigger recomputation. + */ + void SetShadowsEnabled(bool enabled) { + // Only update if the value actually changed to avoid unnecessary uniform buffer updates + if (this->shadowsEnabled != enabled) { + this->shadowsEnabled = enabled; + // TODO: Update uniform buffer with shadow enable/disable flag + // Shadow maps should remain cached and not be recomputed + } + } + + /** + * @brief Set the entity to be highlighted during rendering. + * @param entity The entity to highlight, or nullptr to clear highlighting. + */ + void SetHighlightedEntity(Entity* entity) { + this->highlightedEntity = entity; + } + + /** + * @brief Create or resize light storage buffers to accommodate the given number of lights. + * @param lightCount The number of lights to accommodate. + * @return True if successful, false otherwise. + */ + bool createOrResizeLightStorageBuffers(size_t lightCount); + + /** + * @brief Update light storage buffer with current light data. + * @param frameIndex The current frame index. + * @param lights The light data to upload. + * @return True if successful, false otherwise. + */ + bool updateLightStorageBuffer(uint32_t frameIndex, const std::vector& lights); + + /** + * @brief Update all existing descriptor sets with new light storage buffer references. + * Called when light storage buffers are recreated to ensure descriptor sets reference valid buffers. + */ + void updateAllDescriptorSetsWithNewLightBuffers(); + vk::Format findDepthFormat(); /** @@ -297,6 +402,13 @@ class Renderer { static const std::string SHARED_DEFAULT_OCCLUSION_ID; static const std::string SHARED_DEFAULT_EMISSIVE_ID; + /** + * @brief Determine appropriate texture format based on texture type. + * @param textureId The texture identifier to analyze. + * @return The appropriate Vulkan format (sRGB for baseColor, linear for others). + */ + static vk::Format determineTextureFormat(const std::string& textureId); + private: // Platform Platform* platform = nullptr; @@ -304,6 +416,14 @@ class Renderer { // Model loader reference for accessing extracted lights class ModelLoader* modelLoader = nullptr; + // PBR rendering parameters + float gamma = 2.2f; // Gamma correction value + float exposure = 3.0f; // HDR exposure value (higher for emissive lighting) + bool shadowsEnabled = true; // Shadow rendering enabled by default + + // Entity highlighting + Entity* highlightedEntity = nullptr; + // Vulkan RAII context vk::raii::Context context; @@ -396,12 +516,42 @@ class Renderer { std::unique_ptr textureImageAllocation = nullptr; vk::raii::ImageView textureImageView = nullptr; vk::raii::Sampler textureSampler = nullptr; + vk::Format format = vk::Format::eR8G8B8A8Srgb; // Store texture format for proper color space handling + uint32_t mipLevels = 1; // Store number of mipmap levels }; std::unordered_map textureResources; // Default texture resources (used when no texture is provided) TextureResources defaultTextureResources; + // Shadow mapping resources + struct ShadowMapResources { + vk::raii::Image shadowMapImage = nullptr; + std::unique_ptr shadowMapImageAllocation = nullptr; + vk::raii::ImageView shadowMapImageView = nullptr; + vk::raii::Sampler shadowMapSampler = nullptr; + vk::raii::Framebuffer shadowMapFramebuffer = nullptr; + vk::raii::RenderPass shadowMapRenderPass = nullptr; + uint32_t shadowMapSize = 2048; // Default shadow map resolution + }; + std::vector shadowMaps; // One shadow map per light + + // Shadow mapping constants + static constexpr uint32_t MAX_SHADOW_MAPS = 16; // Match actual shadow map count for performance + static constexpr uint32_t DEFAULT_SHADOW_MAP_SIZE = 2048; + + // Static lights loaded during model initialization + std::vector staticLights; + + // Dynamic lighting system using storage buffers + struct LightStorageBuffer { + vk::raii::Buffer buffer = nullptr; + std::unique_ptr allocation = nullptr; + void* mapped = nullptr; + size_t capacity = 0; // Current capacity in number of lights + size_t size = 0; // Current number of lights + }; + std::vector lightStorageBuffers; // One per frame in flight // Entity resources (contains descriptor sets - must be declared before descriptor pool) struct EntityResources { @@ -455,7 +605,7 @@ class Renderer { bool createInstance(const std::string& appName, bool enableValidationLayers); bool setupDebugMessenger(bool enableValidationLayers); bool createSurface(); - bool checkValidationLayerSupport(); + bool checkValidationLayerSupport() const; bool pickPhysicalDevice(); void addSupportedOptionalExtensions(); bool createLogicalDevice(bool enableValidationLayers); @@ -468,8 +618,16 @@ class Renderer { bool createPBRPipeline(); bool createLightingPipeline(); bool createComputePipeline(); - void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material); + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) const; bool createCommandPool(); + + // Shadow mapping methods + bool createShadowMaps(); + bool createShadowMapRenderPass(); + bool createShadowMapFramebuffers(); + bool createShadowMapDescriptorSetLayout(); + void renderShadowMaps(const std::vector& entities, const std::vector& lights); + void updateShadowMapUniforms(uint32_t lightIndex, const ExtractedLight& light); bool createComputeCommandPool(); bool createDepthResources(); bool createTextureImage(const std::string& texturePath, TextureResources& resources); @@ -507,11 +665,11 @@ class Renderer { void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size); std::pair createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties); - std::pair> createImagePooled(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties); - void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout); - void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height); + std::pair> createImagePooled(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, uint32_t mipLevels = 1); + void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels = 1); + void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector& regions) const; - vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags); + vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels = 1); vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features); bool hasStencilComponent(vk::Format format); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 8d2fcbd1..52d6aca0 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -17,34 +17,18 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { - // // Check if this is a shader debug printf message - // if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) { - // std::string message(pCallbackData->pMessage); - // if (message.find("DEBUG-PRINTF") != std::string::npos) { - // // This is a shader debug printf message - always show it - // std::cout << "FINDME ===== SHADER DEBUG: " << pCallbackData->pMessage << std::endl; - // return VK_FALSE; - // } - // } - printf("Received %s\n", pCallbackData->pMessage); - - // if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { - // // Print message to console - // std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; - // } - - return VK_FALSE; -} - -// Debug callback -static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( - VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - VkDebugUtilsMessageTypeFlagsEXT messageType, - const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, - void* pUserData) { + // Check if this is a shader debug printf message + if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) { + std::string message(pCallbackData->pMessage); + if (message.find("DEBUG-PRINTF") != std::string::npos) { + // This is a shader debug printf message - always show it + std::cout << "FINDME ===== SHADER DEBUG: " << pCallbackData->pMessage << std::endl; + return VK_FALSE; + } + } - if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { - // Print message to console + if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { + // Print a message to the console std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; } @@ -72,7 +56,7 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer vk::detail::DynamicLoader dl; auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); - // Create Vulkan instance + // Create a Vulkan instance if (!createInstance(appName, enableValidationLayers)) { return false; } @@ -184,6 +168,30 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + // Create shadow maps for shadow mapping + if (!createShadowMaps()) { + std::cerr << "Failed to create shadow maps" << std::endl; + return false; + } + + // Create a shadow map render pass + if (!createShadowMapRenderPass()) { + std::cerr << "Failed to create shadow map render pass" << std::endl; + return false; + } + + // Create shadow map framebuffers + if (!createShadowMapFramebuffers()) { + std::cerr << "Failed to create shadow map framebuffers" << std::endl; + return false; + } + + // Create a shadow map descriptor set layout + if (!createShadowMapDescriptorSetLayout()) { + std::cerr << "Failed to create shadow map descriptor set layout" << std::endl; + return false; + } + // Create command buffers if (!createCommandBuffers()) { return false; @@ -329,7 +337,7 @@ bool Renderer::createSurface() { } } -// Pick physical device +// Pick a physical device bool Renderer::pickPhysicalDevice() { try { // Get available physical devices @@ -399,10 +407,9 @@ bool Renderer::pickPhysicalDevice() { addSupportedOptionalExtensions(); return true; - } else { - std::cerr << "Failed to find a suitable GPU. Make sure your GPU supports Vulkan and has the required extensions." << std::endl; - return false; } + std::cerr << "Failed to find a suitable GPU. Make sure your GPU supports Vulkan and has the required extensions." << std::endl; + return false; } catch (const std::exception& e) { std::cerr << "Failed to pick physical device: " << e.what() << std::endl; return false; @@ -417,10 +424,8 @@ void Renderer::addSupportedOptionalExtensions() { // Check which optional extensions are supported and add them to deviceExtensions for (const auto& optionalExt : optionalDeviceExtensions) { - bool supported = false; for (const auto& availableExt : availableExtensions) { if (strcmp(availableExt.extensionName, optionalExt) == 0) { - supported = true; deviceExtensions.push_back(optionalExt); std::cout << "Adding optional extension: " << optionalExt << std::endl; break; @@ -435,9 +440,9 @@ void Renderer::addSupportedOptionalExtensions() { // Create logical device bool Renderer::createLogicalDevice(bool enableValidationLayers) { try { - // Create queue create infos for each unique queue family + // Create queue create info for each unique queue family std::vector queueCreateInfos; - std::set uniqueQueueFamilies = { + std::set uniqueQueueFamilies = { queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.presentFamily.value(), queueFamilyIndices.computeFamily.value() @@ -457,13 +462,39 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { auto features = physicalDevice.getFeatures2(); features.features.samplerAnisotropy = vk::True; + // Explicitly configure device features to prevent validation layer warnings + // These features are required by extensions or other features, so we enable them explicitly + + // Timeline semaphore features (required for synchronization2) + vk::PhysicalDeviceTimelineSemaphoreFeatures timelineSemaphoreFeatures; + timelineSemaphoreFeatures.timelineSemaphore = vk::True; + + // Vulkan memory model features (required for some shader operations) + vk::PhysicalDeviceVulkanMemoryModelFeatures memoryModelFeatures; + memoryModelFeatures.vulkanMemoryModel = vk::True; + memoryModelFeatures.vulkanMemoryModelDeviceScope = vk::True; + + // Buffer device address features (required for some buffer operations) + vk::PhysicalDeviceBufferDeviceAddressFeatures bufferDeviceAddressFeatures; + bufferDeviceAddressFeatures.bufferDeviceAddress = vk::True; + + // 8-bit storage features (required for some shader storage operations) + vk::PhysicalDevice8BitStorageFeatures storage8BitFeatures; + storage8BitFeatures.storageBuffer8BitAccess = vk::True; + // Enable Vulkan 1.3 features vk::PhysicalDeviceVulkan13Features vulkan13Features; vulkan13Features.dynamicRendering = vk::True; vulkan13Features.synchronization2 = vk::True; - features.pNext = &vulkan13Features; - // Create device + // Chain the feature structures together + timelineSemaphoreFeatures.pNext = &memoryModelFeatures; + memoryModelFeatures.pNext = &bufferDeviceAddressFeatures; + bufferDeviceAddressFeatures.pNext = &storage8BitFeatures; + storage8BitFeatures.pNext = &vulkan13Features; + features.pNext = &timelineSemaphoreFeatures; + + // Create a device vk::DeviceCreateInfo createInfo{ .pNext = &features, .queueCreateInfoCount = static_cast(queueCreateInfos.size()), @@ -497,7 +528,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { } // Check validation layer support -bool Renderer::checkValidationLayerSupport() { +bool Renderer::checkValidationLayerSupport() const { // Get available layers std::vector availableLayers = context.enumerateInstanceLayerProperties(); diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index 5c3a2275..7e415f0f 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -6,10 +6,10 @@ // This file contains pipeline-related methods from the Renderer class -// Create descriptor set layout +// Create a descriptor set layout bool Renderer::createDescriptorSetLayout() { try { - // Create binding for uniform buffer + // Create binding for a uniform buffer vk::DescriptorSetLayoutBinding uboLayoutBinding{ .binding = 0, .descriptorType = vk::DescriptorType::eUniformBuffer, @@ -46,7 +46,7 @@ bool Renderer::createDescriptorSetLayout() { bool Renderer::createPBRDescriptorSetLayout() { try { // Create descriptor set layout bindings for PBR shader - std::array bindings = { + std::array bindings = { // Binding 0: Uniform buffer (UBO) vk::DescriptorSetLayoutBinding{ .binding = 0, @@ -94,10 +94,26 @@ bool Renderer::createPBRDescriptorSetLayout() { .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr + }, + // Binding 6: Shadow map array + vk::DescriptorSetLayoutBinding{ + .binding = 6, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = MAX_SHADOW_MAPS, // Array of shadow maps (dynamic count) + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 7: Light storage buffer + vk::DescriptorSetLayoutBinding{ + .binding = 7, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr } }; - // Create descriptor set layout + // Create a descriptor set layout vk::DescriptorSetLayoutCreateInfo layoutInfo{ .bindingCount = static_cast(bindings.size()), .pBindings = bindings.data() @@ -112,27 +128,25 @@ bool Renderer::createPBRDescriptorSetLayout() { } } -// Create graphics pipeline +// Create a graphics pipeline bool Renderer::createGraphicsPipeline() { try { // Read shader code - auto vertShaderCode = readFile("shaders/texturedMesh.spv"); - auto fragShaderCode = readFile("shaders/texturedMesh.spv"); + auto shaderCode = readFile("shaders/texturedMesh.spv"); // Create shader modules - vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); // Create shader stage info vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eVertex, - .module = *vertShaderModule, + .module = *shaderModule, .pName = "VSMain" }; vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, - .module = *fragShaderModule, + .module = *shaderModule, .pName = "PSMain" }; @@ -187,7 +201,7 @@ bool Renderer::createGraphicsPipeline() { .stencilTestEnable = VK_FALSE }; - // Create color blend attachment state + // Create a color blend attachment state vk::PipelineColorBlendAttachmentState colorBlendAttachment{ .blendEnable = VK_FALSE, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA @@ -202,7 +216,7 @@ bool Renderer::createGraphicsPipeline() { }; // Create dynamic state info - std::vector dynamicStates = { + std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; @@ -275,23 +289,21 @@ bool Renderer::createPBRPipeline() { } // Read shader code - auto vertShaderCode = readFile("shaders/pbr.spv"); - auto fragShaderCode = readFile("shaders/pbr.spv"); + auto shaderCode = readFile("shaders/pbr.spv"); // Create shader modules - vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); // Create shader stage info vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eVertex, - .module = *vertShaderModule, + .module = *shaderModule, .pName = "VSMain" }; vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, - .module = *fragShaderModule, + .module = *shaderModule, .pName = "PSMain" }; @@ -305,7 +317,7 @@ bool Renderer::createPBRPipeline() { }; // Define vertex attribute descriptions - std::array attributeDescriptions = { + std::array attributeDescriptions = { // Position attribute vk::VertexInputAttributeDescription{ .location = 0, @@ -381,7 +393,7 @@ bool Renderer::createPBRPipeline() { .stencilTestEnable = VK_FALSE }; - // Create color blend attachment state + // Create a color blend attachment state vk::PipelineColorBlendAttachmentState colorBlendAttachment{ .blendEnable = VK_FALSE, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA @@ -396,7 +408,7 @@ bool Renderer::createPBRPipeline() { }; // Create dynamic state info - std::vector dynamicStates = { + std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; @@ -413,7 +425,7 @@ bool Renderer::createPBRPipeline() { .size = sizeof(MaterialProperties) }; - // Create pipeline layout using the PBR descriptor set layout + // Create a pipeline layout using the PBR descriptor set layout vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, .pSetLayouts = &*pbrDescriptorSetLayout, @@ -436,7 +448,7 @@ bool Renderer::createPBRPipeline() { .stencilAttachmentFormat = vk::Format::eUndefined }; - // Create graphics pipeline + // Create a graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &pbrPipelineRenderingCreateInfo, @@ -466,27 +478,25 @@ bool Renderer::createPBRPipeline() { } } -// Create lighting pipeline +// Create a lighting pipeline bool Renderer::createLightingPipeline() { try { // Read shader code - auto vertShaderCode = readFile("shaders/lighting.spv"); - auto fragShaderCode = readFile("shaders/lighting.spv"); + auto shaderCode = readFile("shaders/lighting.spv"); // Create shader modules - vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); // Create shader stage info vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eVertex, - .module = *vertShaderModule, + .module = *shaderModule, .pName = "VSMain" }; vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, - .module = *fragShaderModule, + .module = *shaderModule, .pName = "PSMain" }; @@ -541,7 +551,7 @@ bool Renderer::createLightingPipeline() { .stencilTestEnable = VK_FALSE }; - // Create color blend attachment state + // Create a color blend attachment state vk::PipelineColorBlendAttachmentState colorBlendAttachment{ .blendEnable = VK_TRUE, .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, @@ -562,7 +572,7 @@ bool Renderer::createLightingPipeline() { }; // Create dynamic state info - std::vector dynamicStates = { + std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; @@ -602,7 +612,7 @@ bool Renderer::createLightingPipeline() { .stencilAttachmentFormat = vk::Format::eUndefined }; - // Create graphics pipeline + // Create a graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &lightingPipelineRenderingCreateInfo, @@ -633,6 +643,6 @@ bool Renderer::createLightingPipeline() { } // Push material properties to the pipeline -void Renderer::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) { +void Renderer::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) const { commandBuffer.pushConstants(*pbrPipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(MaterialProperties), &material); } diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index fd53d9a6..f2aaa2b1 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // This file contains rendering-related methods from the Renderer class @@ -273,11 +274,12 @@ void Renderer::cleanupSwapChain() { void Renderer::recreateSwapChain() { // Wait for all frames in flight to complete before recreating the swap chain std::vector allFences; + allFences.reserve(inFlightFences.size()); for (const auto& fence : inFlightFences) { allFences.push_back(*fence); } if (!allFences.empty()) { - device.waitForFences(allFences, VK_TRUE, UINT64_MAX); + if (device.waitForFences(allFences, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} } // Wait for the device to be idle before recreating the swap chain @@ -299,11 +301,11 @@ void Renderer::recreateSwapChain() { // Wait for all command buffers to complete before clearing resources for (const auto& fence : inFlightFences) { - device.waitForFences(*fence, VK_TRUE, UINT64_MAX); + if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} } - // Clear all entity descriptor sets since they're now invalid (allocated from old pool) - for (auto& [entity, resources] : entityResources) { + // Clear all entity descriptor sets since they're now invalid (allocated from the old pool) + for (auto& resources : entityResources | std::views::values) { resources.basicDescriptorSets.clear(); resources.pbrDescriptorSets.clear(); } @@ -334,33 +336,37 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera ubo.proj = camera->GetProjectionMatrix(); ubo.proj[1][1] *= -1; // Flip Y for Vulkan - // Set up lighting from extracted GLTF lights or fallback to default - std::vector extractedLights; - if (modelLoader) { - extractedLights = modelLoader->GetExtractedLights("../Assets/Bistro.glb"); - } + // Use static lights loaded during model initialization + const std::vector& extractedLights = staticLights; if (!extractedLights.empty()) { - // Fill up to 4 lights from extracted lights - for (size_t i = 0; i < 4 && i < extractedLights.size(); ++i) { - const auto& light = extractedLights[i]; - ubo.lightPositions[i] = glm::vec4(light.position, 1.0f); - ubo.lightColors[i] = glm::vec4(light.color * light.intensity, 1.0f); - } + // Use all available lights (no hardcoded limit) + size_t numLights = extractedLights.size(); + + // Update the light storage buffer with all light data + updateLightStorageBuffer(currentImage, extractedLights); - // Fill remaining slots with zero-intensity lights (no contribution) - for (size_t i = extractedLights.size(); i < 4; ++i) { - ubo.lightPositions[i] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - ubo.lightColors[i] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); // No light contribution + ubo.lightCount = static_cast(numLights); + // Set shadow map count based on shadowsEnabled flag + if (shadowsEnabled) { + ubo.shadowMapCount = static_cast(std::min(numLights, size_t(16))); // Limit shadow maps to 16 for performance (indices 0-15) + } else { + ubo.shadowMapCount = 0; // Disable shadows when shadowsEnabled is false } + } else { + ubo.lightCount = 0; + ubo.shadowMapCount = 0; } + // Set shadow mapping parameters + ubo.shadowBias = 0.005f; // Adjust to prevent shadow acne + // Set camera position for PBR calculations ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); - // Set PBR parameters - ubo.exposure = 1.0f; - ubo.gamma = 2.2f; + // Set PBR parameters (use member variables for UI control) + ubo.exposure = this->exposure; + ubo.gamma = this->gamma; ubo.prefilteredCubeMipLevels = 0.0f; ubo.scaleIBLAmbient = 1.0f; @@ -378,7 +384,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Use RAII to ensure rendering state is always reset, even if an exception occurs struct RenderingStateGuard { MemoryPool* pool; - RenderingStateGuard(MemoryPool* p) : pool(p) {} + explicit RenderingStateGuard(MemoryPool* p) : pool(p) {} ~RenderingStateGuard() { if (pool) { pool->setRenderingActive(false); @@ -387,7 +393,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam } guard(memoryPool.get()); // Wait for the previous frame to finish - device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + if (device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} // Acquire the next image from the swap chain uint32_t imageIndex; @@ -407,7 +413,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam recreateSwapChain(); return; - } else if (result.first != vk::Result::eSuccess) { + } + if (result.first != vk::Result::eSuccess) { throw std::runtime_error("Failed to acquire swap chain image"); } @@ -486,8 +493,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam continue; } - // Determine which pipeline to use based on ImGui PBR toggle and entity type - bool usePBR = imguiSystem && imguiSystem->IsPBREnabled() && (entity->GetName().find("Bistro") == 0); + // Determine which pipeline to use - now defaults to BRDF/PBR instead of Phong + // Use basic pipeline only when PBR is explicitly disabled via ImGui + bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); + bool usePBR = !useBasic; // BRDF/PBR is now the default lighting model vk::raii::Pipeline* selectedPipeline = usePBR ? &pbrGraphicsPipeline : &graphicsPipeline; vk::raii::PipelineLayout* selectedLayout = usePBR ? &pbrPipelineLayout : &pipelineLayout; @@ -556,6 +565,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam int emissiveTextureSet; float alphaMask; float alphaMaskCutoff; + glm::vec3 emissiveFactor; // Add emissive factor for HDR emissive sources + float emissiveStrength; // KHR_materials_emissive_strength extension }; // Set PBR material properties using push constants @@ -563,10 +574,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam PushConstants pushConstants{}; // Try to get material properties for this specific entity - if (modelLoader && entity->GetName().find("Bistro_Material_") == 0) { - // Extract material name from entity name for Bistro entities + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + // Extract material name from entity name for any GLTF model entities std::string entityName = entity->GetName(); - size_t materialStart = entityName.find("Bistro_Material_"); + size_t materialStart = entityName.find("_Material_"); if (materialStart != std::string::npos) { // Try to extract material name from entity name size_t nameStart = entityName.find_last_of("_"); @@ -578,6 +589,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam pushConstants.baseColorFactor = glm::vec4(material->albedo, 1.0f); pushConstants.metallicFactor = material->metallic; pushConstants.roughnessFactor = material->roughness; + pushConstants.emissiveFactor = material->emissive; // Set emissive factor for HDR emissive sources + pushConstants.emissiveStrength = material->emissiveStrength; // Set emissive strength from KHR_materials_emissive_strength extension } else { // Fallback: Use entity-specific variation size_t hash = std::hash{}(entityName); @@ -585,6 +598,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam pushConstants.baseColorFactor = glm::vec4(0.7f + variation * 0.3f, 0.6f + variation * 0.4f, 0.5f + variation * 0.5f, 1.0f); pushConstants.metallicFactor = variation * 0.8f; pushConstants.roughnessFactor = 0.2f + variation * 0.6f; + pushConstants.emissiveFactor = glm::vec3(0.0f); // No emissive for fallback materials + pushConstants.emissiveStrength = 1.0f; // Default emissive strength } } } @@ -593,6 +608,19 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); pushConstants.metallicFactor = 0.1f; pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); // No emissive for default materials + pushConstants.emissiveStrength = 1.0f; // Default emissive strength + } + + // Apply highlighting effect if this entity is highlighted + if (highlightedEntity == entity) { + // Add a bright yellow tint to highlight the entity + pushConstants.baseColorFactor.r = std::min(1.0f, pushConstants.baseColorFactor.r + 0.3f); + pushConstants.baseColorFactor.g = std::min(1.0f, pushConstants.baseColorFactor.g + 0.3f); + pushConstants.baseColorFactor.b = std::min(1.0f, pushConstants.baseColorFactor.b * 0.7f); // Reduce blue for yellow tint + // Also add some emissive glow for extra visibility + pushConstants.emissiveFactor = glm::vec3(0.2f, 0.2f, 0.0f); + pushConstants.emissiveStrength = 2.0f; } // Set texture binding indices diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 66b57803..281bd492 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1,5 +1,6 @@ #include "renderer.h" #include "model_loader.h" +#include "mesh_component.h" #include #include #include @@ -10,6 +11,10 @@ #define STB_IMAGE_IMPLEMENTATION #include +// KTX2 support +#include +#include + // This file contains resource-related methods from the Renderer class // Define shared default PBR texture identifiers (static constants) @@ -66,15 +71,95 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc return true; } - // Load texture image + // Check if this is a KTX2 file + bool isKtx2 = texturePath.find(".ktx2") != std::string::npos; + int texWidth, texHeight, texChannels; - stbi_uc* pixels = stbi_load(texturePath.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); - if (!pixels) { - std::cerr << "Failed to load texture image: " << texturePath << std::endl; - return false; - } + stbi_uc* pixels = nullptr; + ktxTexture2* ktxTex = nullptr; + vk::DeviceSize imageSize; + + uint32_t mipLevels = 1; + std::vector copyRegions; + + if (isKtx2) { + // Load KTX2 file + KTX_error_code result = ktxTexture2_CreateFromNamedFile(texturePath.c_str(), + KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, + &ktxTex); + if (result != KTX_SUCCESS) { + std::cerr << "Failed to load KTX2 texture: " << texturePath << " (error: " << result << ")" << std::endl; + return false; + } + + // Check if texture needs transcoding (Basis Universal compressed) + bool wasTranscoded = ktxTexture2_NeedsTranscoding(ktxTex); + if (wasTranscoded) { + // Transcode to RGBA8 uncompressed format for Vulkan compatibility + ktx_transcode_fmt_e transcodeFormat = KTX_TTF_RGBA32; + + result = ktxTexture2_TranscodeBasis(ktxTex, transcodeFormat, 0); + if (result != KTX_SUCCESS) { + std::cerr << "Failed to transcode KTX2 texture: " << texturePath << " (error: " << result << ")" << std::endl; + ktxTexture_Destroy((ktxTexture*)ktxTex); + return false; + } + } + + texWidth = ktxTex->baseWidth; + texHeight = ktxTex->baseHeight; + texChannels = 4; // KTX2 textures are typically RGBA + // Disable mipmapping - only use base level (level 0) + mipLevels = 1; + + // Calculate size for base level only + if (wasTranscoded) { + imageSize = texWidth * texHeight * 4; // RGBA = 4 bytes per pixel + } else { + imageSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); // Only level 0 + } - vk::DeviceSize imageSize = texWidth * texHeight * 4; + // Create single copy region for base level only + copyRegions.push_back({ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {static_cast(texWidth), static_cast(texHeight), 1} + }); + + std::cout << "Loaded KTX2 texture: " << texturePath + << " (" << texWidth << "x" << texHeight << ", base level only, size: " << imageSize << " bytes)" << std::endl; + } else { + // Load standard image formats using stb_image + pixels = stbi_load(texturePath.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + if (!pixels) { + std::cerr << "Failed to load texture image: " << texturePath << std::endl; + return false; + } + imageSize = texWidth * texHeight * 4; + + // Create single copy region for non-KTX2 textures + copyRegions.push_back({ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {static_cast(texWidth), static_cast(texHeight), 1} + }); + } // Create staging buffer auto [stagingBuffer, stagingBufferMemory] = createBuffer( @@ -85,17 +170,51 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc // Copy pixel data to staging buffer void* data = stagingBufferMemory.mapMemory(0, imageSize); - memcpy(data, pixels, static_cast(imageSize)); + + if (isKtx2) { + // Copy KTX2 texture data for base level only (level 0) + size_t levelSize; + const void* levelData; + + if (ktxTexture2_NeedsTranscoding(ktxTex)) { + // For transcoded textures, get data from the transcoded buffer + levelSize = texWidth * texHeight * 4; // RGBA = 4 bytes per pixel + ktx_size_t offset; + ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); + levelData = ktxTexture_GetData((ktxTexture*)ktxTex) + offset; + } else { + // For non-transcoded textures, get data directly + levelSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); + ktx_size_t offset; + ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); + levelData = ktxTexture_GetData((ktxTexture*)ktxTex) + offset; + } + + memcpy(data, levelData, levelSize); + } else { + // Copy regular image data + memcpy(data, pixels, static_cast(imageSize)); + } + stagingBufferMemory.unmapMemory(); // Free pixel data - stbi_image_free(pixels); + if (isKtx2) { + ktxTexture_Destroy((ktxTexture*)ktxTex); + } else { + stbi_image_free(pixels); + } + + // Determine appropriate texture format based on texture type + vk::Format textureFormat = Renderer::determineTextureFormat(texturePath); + std::cout << "Loading external texture " << texturePath << " with format: " + << (textureFormat == vk::Format::eR8G8B8A8Srgb ? "sRGB" : "Linear") << std::endl; // Create texture image using memory pool auto [textureImg, textureImgAllocation] = createImagePooled( texWidth, texHeight, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal @@ -107,9 +226,10 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc // Transition image layout for copy transitionImageLayout( *resources.textureImage, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageLayout::eUndefined, - vk::ImageLayout::eTransferDstOptimal + vk::ImageLayout::eTransferDstOptimal, + mipLevels ); // Copy buffer to image @@ -117,17 +237,23 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc *stagingBuffer, *resources.textureImage, static_cast(texWidth), - static_cast(texHeight) + static_cast(texHeight), + copyRegions ); // Transition image layout for shader access transitionImageLayout( *resources.textureImage, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageLayout::eTransferDstOptimal, - vk::ImageLayout::eShaderReadOnlyOptimal + vk::ImageLayout::eShaderReadOnlyOptimal, + mipLevels ); + // Store the format and mipLevels for createTextureImageView + resources.format = textureFormat; + resources.mipLevels = mipLevels; + // Create texture image view if (!createTextureImageView(resources)) { return false; @@ -153,8 +279,9 @@ bool Renderer::createTextureImageView(TextureResources& resources) { try { resources.textureImageView = createImageView( resources.textureImage, - vk::Format::eR8G8B8A8Srgb, - vk::ImageAspectFlagBits::eColor + resources.format, // Use the stored format instead of hardcoded sRGB + vk::ImageAspectFlagBits::eColor, + resources.mipLevels // Use the stored mipLevels ); return true; } catch (const std::exception& e) { @@ -166,9 +293,6 @@ bool Renderer::createTextureImageView(TextureResources& resources) { // Create shared default PBR textures (to avoid creating hundreds of identical textures) bool Renderer::createSharedDefaultPBRTextures() { try { - std::cout << "Creating shared default PBR textures..." << std::endl; - - // Create shared default albedo texture (neutral gray to reduce excessive brightness) unsigned char whitePixel[4] = {128, 128, 128, 255}; // 50% gray instead of pure white if (!LoadTextureFromMemory(SHARED_DEFAULT_ALBEDO_ID, whitePixel, 1, 1, 4)) { std::cerr << "Failed to create shared default albedo texture" << std::endl; @@ -255,11 +379,25 @@ bool Renderer::createDefaultTextureResources() { ); // Copy buffer to image + std::vector regions = {{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + }}; copyBufferToImage( *stagingBuffer, *defaultTextureResources.textureImage, width, - height + height, + regions ); // Transition image layout for shader access @@ -291,11 +429,11 @@ bool Renderer::createTextureSampler(TextureResources& resources) { // Get physical device properties vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); - // Create sampler + // Create sampler (mipmapping disabled) vk::SamplerCreateInfo samplerInfo{ .magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear, - .mipmapMode = vk::SamplerMipmapMode::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eNearest, // Disable mipmap filtering .addressModeU = vk::SamplerAddressMode::eRepeat, .addressModeV = vk::SamplerAddressMode::eRepeat, .addressModeW = vk::SamplerAddressMode::eRepeat, @@ -305,7 +443,7 @@ bool Renderer::createTextureSampler(TextureResources& resources) { .compareEnable = VK_FALSE, .compareOp = vk::CompareOp::eAlways, .minLod = 0.0f, - .maxLod = 0.0f, + .maxLod = 0.0f, // Force single mip level (no mipmapping) .borderColor = vk::BorderColor::eIntOpaqueBlack, .unnormalizedCoordinates = VK_FALSE }; @@ -347,6 +485,21 @@ bool Renderer::LoadTexture(const std::string& texturePath) { return success; } +// Determine appropriate texture format based on texture type +vk::Format Renderer::determineTextureFormat(const std::string& textureId) { + // BaseColor/Albedo textures should be in sRGB space for proper gamma correction + if (textureId.find("BaseColor") != std::string::npos || + textureId.find("Albedo") != std::string::npos || + textureId.find("Diffuse") != std::string::npos || + textureId == Renderer::SHARED_DEFAULT_ALBEDO_ID) { + return vk::Format::eR8G8B8A8Srgb; + } + + // All other PBR textures (normal, metallic-roughness, occlusion, emissive) should be linear + // because they contain non-color data that shouldn't be gamma corrected + return vk::Format::eR8G8B8A8Unorm; +} + // Load texture from raw image data in memory bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigned char* imageData, int width, int height, int channels) { @@ -369,7 +522,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne int targetChannels = 4; // Always use RGBA for consistency vk::DeviceSize imageSize = width * height * targetChannels; - // Create staging buffer + // Create a staging buffer auto [stagingBuffer, stagingBufferMemory] = createBuffer( imageSize, vk::BufferUsageFlagBits::eTransferSrc, @@ -378,7 +531,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne // Copy and convert pixel data to staging buffer void* data = stagingBufferMemory.mapMemory(0, imageSize); - unsigned char* stagingData = static_cast(data); + auto* stagingData = static_cast(data); if (channels == 4) { // Already RGBA, direct copy @@ -415,11 +568,13 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne stagingBufferMemory.unmapMemory(); + // Determine the appropriate texture format based on the texture type + vk::Format textureFormat = determineTextureFormat(textureId); // Create texture image using memory pool auto [textureImg, textureImgAllocation] = createImagePooled( width, height, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal @@ -431,31 +586,48 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne // Transition image layout for copy transitionImageLayout( *resources.textureImage, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal ); // Copy buffer to image + std::vector regions = {{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {static_cast(width), static_cast(height), 1} + }}; copyBufferToImage( *stagingBuffer, *resources.textureImage, static_cast(width), - static_cast(height) + static_cast(height), + regions ); // Transition image layout for shader access transitionImageLayout( *resources.textureImage, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal ); + // Store the format for createTextureImageView + resources.format = textureFormat; + // Create texture image view resources.textureImageView = createImageView( resources.textureImage, - vk::Format::eR8G8B8A8Srgb, + textureFormat, vk::ImageAspectFlagBits::eColor ); @@ -559,122 +731,6 @@ bool Renderer::createMeshResources(MeshComponent* meshComponent) { } } -// REMOVED: Create material-specific mesh resources for PBR rendering - Memory inefficient approach -// This function was creating separate vertex/index buffers for each material, multiplying memory usage -/* -bool Renderer::createMaterialMeshResources(const std::string& modelName) { - try { - // Check if material mesh resources already exist - auto it = materialMeshResources.find(modelName); - if (it != materialMeshResources.end()) { - return true; - } - - // Get material meshes from model loader - if (!modelLoader) { - std::cerr << "ModelLoader not available for creating material mesh resources" << std::endl; - return false; - } - - std::vector materialMeshes = modelLoader->GetMaterialMeshes(modelName); - if (materialMeshes.empty()) { - std::cerr << "No material meshes found for model: " << modelName << std::endl; - return false; - } - - std::cout << "Creating material mesh resources for model: " << modelName - << " (" << materialMeshes.size() << " materials)" << std::endl; - - // Create material mesh resources - MaterialMeshResources resources; - resources.materialMeshes.reserve(materialMeshes.size()); - resources.materialNames.reserve(materialMeshes.size()); - resources.materialIndices.reserve(materialMeshes.size()); - - for (const auto& materialMesh : materialMeshes) { - const auto& vertices = materialMesh.vertices; - const auto& indices = materialMesh.indices; - - if (vertices.empty() || indices.empty()) { - std::cerr << "Material mesh has no vertices or indices: " << materialMesh.materialName << std::endl; - continue; - } - - std::cout << " Creating buffers for material: " << materialMesh.materialName - << " (" << vertices.size() << " vertices, " << indices.size() << " indices)" << std::endl; - - // Create vertex buffer - vk::DeviceSize vertexBufferSize = sizeof(vertices[0]) * vertices.size(); - auto [stagingVertexBuffer, stagingVertexBufferMemory] = createBuffer( - vertexBufferSize, - vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - // Copy vertex data to staging buffer - void* vertexData = stagingVertexBufferMemory.mapMemory(0, vertexBufferSize); - memcpy(vertexData, vertices.data(), static_cast(vertexBufferSize)); - stagingVertexBufferMemory.unmapMemory(); - - // Create vertex buffer on device - auto [vertexBuffer, vertexBufferMemory] = createBuffer( - vertexBufferSize, - vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - // Copy from staging buffer to device buffer - copyBuffer(stagingVertexBuffer, vertexBuffer, vertexBufferSize); - - // Create index buffer - vk::DeviceSize indexBufferSize = sizeof(indices[0]) * indices.size(); - auto [stagingIndexBuffer, stagingIndexBufferMemory] = createBuffer( - indexBufferSize, - vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - // Copy index data to staging buffer - void* indexData = stagingIndexBufferMemory.mapMemory(0, indexBufferSize); - memcpy(indexData, indices.data(), static_cast(indexBufferSize)); - stagingIndexBufferMemory.unmapMemory(); - - // Create index buffer on device - auto [indexBuffer, indexBufferMemory] = createBuffer( - indexBufferSize, - vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - // Copy from staging buffer to device buffer - copyBuffer(stagingIndexBuffer, indexBuffer, indexBufferSize); - - // Create mesh resources for this material - MeshResources meshRes; - meshRes.vertexBuffer = std::move(vertexBuffer); - meshRes.vertexBufferMemory = std::move(vertexBufferMemory); - meshRes.indexBuffer = std::move(indexBuffer); - meshRes.indexBufferMemory = std::move(indexBufferMemory); - meshRes.indexCount = static_cast(indices.size()); - - // Store the mesh resources and metadata - resources.materialMeshes.emplace_back(std::move(meshRes)); - resources.materialNames.emplace_back(materialMesh.materialName); - resources.materialIndices.emplace_back(materialMesh.materialIndex); - } - - // Add to material mesh resources map - materialMeshResources[modelName] = std::move(resources); - - std::cout << "Successfully created material mesh resources for model: " << modelName << std::endl; - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create material mesh resources: " << e.what() << std::endl; - return false; - } -} -*/ - // Create uniform buffers bool Renderer::createUniformBuffers(Entity* entity) { try { @@ -720,21 +776,25 @@ bool Renderer::createUniformBuffers(Entity* entity) { // Create descriptor pool bool Renderer::createDescriptorPool() { try { - // Calculate pool sizes for all Bistro materials (131) plus additional entities + // Calculate pool sizes for all Bistro materials plus additional entities + // The Bistro model creates many more entities than initially expected // Each entity needs descriptor sets for both basic and PBR pipelines - // PBR pipeline needs 6 descriptors per set (1 UBO + 5 textures) + // PBR pipeline needs 7 descriptors per set (1 UBO + 5 PBR textures + 1 shadow map array with 16 shadow maps) // Basic pipeline needs 2 descriptors per set (1 UBO + 1 texture) - const uint32_t maxEntities = 200; // Support up to 200 entities (131 Bistro + extras) + const uint32_t maxEntities = 2500; // Increased to 2500 entities to handle large models like bistro with extra safety margin const uint32_t maxDescriptorSets = MAX_FRAMES_IN_FLIGHT * maxEntities * 2; // 2 pipeline types per entity // Calculate descriptor counts // UBO descriptors: 1 per descriptor set const uint32_t uboDescriptors = maxDescriptorSets; - // Texture descriptors: Basic pipeline uses 1, PBR uses 5, so average ~3 per set - // But allocate for worst case: all entities using PBR (5 textures each) - const uint32_t textureDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities * 5; - - std::array poolSizes = { + // Texture descriptors: Basic pipeline uses 1, PBR uses 21 (5 PBR textures + 16 shadow maps) + // Allocate for worst case: all entities using PBR (21 texture descriptors each) + const uint32_t textureDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities * 21; + // Storage buffer descriptors: PBR pipeline uses 1 light storage buffer per descriptor set + // Only PBR entities need storage buffers, so allocate for all entities using PBR + const uint32_t storageBufferDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities; + + std::array poolSizes = { vk::DescriptorPoolSize{ .type = vk::DescriptorType::eUniformBuffer, .descriptorCount = uboDescriptors @@ -742,6 +802,10 @@ bool Renderer::createDescriptorPool() { vk::DescriptorPoolSize{ .type = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = textureDescriptors + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eStorageBuffer, + .descriptorCount = storageBufferDescriptors } }; @@ -753,11 +817,6 @@ bool Renderer::createDescriptorPool() { .pPoolSizes = poolSizes.data() }; - std::cout << "Creating descriptor pool with capacity for " << maxEntities << " entities:" << std::endl; - std::cout << " Max descriptor sets: " << maxDescriptorSets << std::endl; - std::cout << " UBO descriptors: " << uboDescriptors << std::endl; - std::cout << " Texture descriptors: " << textureDescriptors << std::endl; - descriptorPool = vk::raii::DescriptorPool(device, poolInfo); return true; } catch (const std::exception& e) { @@ -776,29 +835,43 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa return false; } - // Get texture resources - don't move them out of the map! + // Get texture resources - use default texture as fallback if specific texture fails TextureResources* textureRes = nullptr; if (!texturePath.empty()) { auto textureIt = textureResources.find(texturePath); if (textureIt == textureResources.end()) { - // Create texture resources if they don't exist + // Try to create texture resources TextureResources tempRes; if (!createTextureImage(texturePath, tempRes)) { - return false; - } - // The texture should now be in the map, find it again - textureIt = textureResources.find(texturePath); - if (textureIt == textureResources.end()) { - std::cerr << "Failed to find texture after creation: " << texturePath << std::endl; - return false; + // Texture loading failed - use default texture instead of failing descriptor set creation + std::cerr << "Warning: Failed to load texture " << texturePath + << " for entity " << entity->GetName() + << ", using default texture instead" << std::endl; + + // Use default texture resources instead + textureRes = &defaultTextureResources; + } else { + // Texture loaded successfully, find it in the map + textureIt = textureResources.find(texturePath); + if (textureIt == textureResources.end()) { + std::cerr << "Warning: Failed to find texture after creation: " << texturePath + << ", using default texture instead" << std::endl; + textureRes = &defaultTextureResources; + } else { + textureRes = &textureIt->second; + } } + } else { + textureRes = &textureIt->second; } - textureRes = &textureIt->second; + } else { + // No texture path specified, use default texture + textureRes = &defaultTextureResources; } // Create descriptor sets using RAII - choose layout based on pipeline type vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; - std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); + std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), @@ -818,7 +891,6 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa targetDescriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { targetDescriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); - std::cout << "Created " << (usePBR ? "PBR" : "basic") << " descriptor set " << i << " with handle: " << *targetDescriptorSets[i] << std::endl; } } @@ -832,11 +904,12 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa }; if (usePBR) { - // PBR pipeline: Create 6 descriptor writes (UBO + 5 textures) - std::array descriptorWrites; + // PBR pipeline: Create 8 descriptor writes (UBO + 5 textures + shadow map array + light storage buffer) + std::array descriptorWrites; std::array imageInfos; + std::array shadowMapInfos; - // Uniform buffer descriptor write (binding 0) + // Uniform buffer descriptor writes (binding 0) descriptorWrites[0] = vk::WriteDescriptorSet{ .dstSet = targetDescriptorSets[i], .dstBinding = 0, @@ -846,31 +919,47 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .pBufferInfo = &bufferInfo }; - // Try to use loaded textures instead of default white texture - vk::DescriptorImageInfo defaultImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + // Get all PBR texture paths from the entity's MeshComponent + auto meshComponent = entity->GetComponent(); + std::vector pbrTexturePaths = { + // Binding 1: baseColor - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) ? + meshComponent->GetBaseColorTexturePath() : SHARED_DEFAULT_ALBEDO_ID, + // Binding 2: metallicRoughness - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) ? + meshComponent->GetMetallicRoughnessTexturePath() : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID, + // Binding 3: normal - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetNormalTexturePath().empty()) ? + meshComponent->GetNormalTexturePath() : SHARED_DEFAULT_NORMAL_ID, + // Binding 4: occlusion - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetOcclusionTexturePath().empty()) ? + meshComponent->GetOcclusionTexturePath() : SHARED_DEFAULT_OCCLUSION_ID, + // Binding 5: emissive - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) ? + meshComponent->GetEmissiveTexturePath() : SHARED_DEFAULT_EMISSIVE_ID }; - // Use entity-specific texture if available, otherwise fall back to default - bool hasValidTexture = !texturePath.empty() && - textureResources.find(texturePath) != textureResources.end(); - + // Create image infos for each PBR texture binding for (int j = 0; j < 5; j++) { - if (hasValidTexture) { - // Use the entity's specific texture for all PBR bindings - const auto& texRes = textureResources.at(texturePath); + const std::string& currentTexturePath = pbrTexturePaths[j]; + + // Find the texture resources for this binding + auto textureIt = textureResources.find(currentTexturePath); + if (textureIt != textureResources.end()) { + // Use the specific texture for this binding + const auto& texRes = textureIt->second; imageInfos[j] = vk::DescriptorImageInfo{ .sampler = *texRes.textureSampler, .imageView = *texRes.textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal }; - std::cout << "Using entity texture for PBR binding " << (j+1) << ": " << texturePath << std::endl; } else { - // Fall back to default texture - imageInfos[j] = defaultImageInfo; - std::cout << "Using default texture for PBR binding " << (j+1) << std::endl; + // Fall back to default white texture if the specific texture is not found + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; } } @@ -886,7 +975,78 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa }; } - // Update descriptor sets with all 6 descriptors + // Create shadow map image infos for binding 6 + for (int j = 0; j < 16; j++) { + if (j < static_cast(shadowMaps.size()) && *shadowMaps[j].shadowMapImageView) { + // Use actual shadow map + shadowMapInfos[j] = vk::DescriptorImageInfo{ + .sampler = *shadowMaps[j].shadowMapSampler, + .imageView = *shadowMaps[j].shadowMapImageView, + .imageLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal + }; + } else { + // Use default texture as placeholder for unused shadow map slots + shadowMapInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + } + + // Create descriptor write for shadow map array (binding 6) + descriptorWrites[6] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 6, + .dstArrayElement = 0, + .descriptorCount = 16, // Array of 16 shadow maps + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = shadowMapInfos.data() + }; + + // Create descriptor write for light storage buffer (binding 7) + // Check if light storage buffers are initialized + if (i < lightStorageBuffers.size() && *lightStorageBuffers[i].buffer) { + vk::DescriptorBufferInfo lightBufferInfo{ + .buffer = *lightStorageBuffers[i].buffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + descriptorWrites[7] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 7, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightBufferInfo + }; + } else { + // Ensure light storage buffers are initialized before creating descriptor sets + // Initialize with at least 1 light to create the buffers + if (!createOrResizeLightStorageBuffers(1)) { + std::cerr << "Failed to initialize light storage buffers for descriptor set creation" << std::endl; + return false; + } + + // Now use the properly initialized light storage buffer + vk::DescriptorBufferInfo lightBufferInfo{ + .buffer = *lightStorageBuffers[i].buffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + descriptorWrites[7] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 7, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightBufferInfo + }; + } + + // Update descriptor sets with all 8 descriptors device.updateDescriptorSets(descriptorWrites, {}); } else { // Basic pipeline: Create 2 descriptor writes (UBO + 1 texture) @@ -1175,15 +1335,16 @@ std::pair> Renderer::cr vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, - vk::MemoryPropertyFlags properties) { + vk::MemoryPropertyFlags properties, + uint32_t mipLevels) { try { if (!memoryPool) { throw std::runtime_error("Memory pool not initialized"); } - // Use memory pool for allocation + // Use memory pool for allocation (mipmap support limited by memory pool API) auto [image, allocation] = memoryPool->createImage(width, height, format, tiling, usage, properties); - std::cout << "Created image using memory pool: " << width << "x" << height << " format=" << static_cast(format) << std::endl; + std::cout << "Created image using memory pool: " << width << "x" << height << " format=" << static_cast(format) << " mipLevels=" << mipLevels << std::endl; return {std::move(image), std::move(allocation)}; @@ -1193,8 +1354,8 @@ std::pair> Renderer::cr } } -// Create image view -vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags) { +// Create an image view +vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels) { try { // Create image view vk::ImageViewCreateInfo viewInfo{ @@ -1204,13 +1365,13 @@ vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format .subresourceRange = { .aspectMask = aspectFlags, .baseMipLevel = 0, - .levelCount = 1, + .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = 1 } }; - return vk::raii::ImageView(device, viewInfo); + return { device, viewInfo }; } catch (const std::exception& e) { std::cerr << "Failed to create image view: " << e.what() << std::endl; throw; @@ -1218,9 +1379,9 @@ vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format } // Transition image layout -void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { +void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels) { try { - // Create command buffer using RAII + // Create a command buffer using RAII vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, @@ -1237,7 +1398,7 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima commandBuffer.begin(beginInfo); - // Create image barrier + // Create an image barrier vk::ImageMemoryBarrier barrier{ .oldLayout = oldLayout, .newLayout = newLayout, @@ -1249,7 +1410,7 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, - .levelCount = 1, + .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = 1 } @@ -1275,13 +1436,20 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima barrier.srcAccessMask = vk::AccessFlagBits::eNone; barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) { + // Support for shadow map creation: transition from undefined to read-only depth layout + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead; + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; } else { throw std::invalid_argument("Unsupported layout transition!"); } - // Add barrier to command buffer + // Add a barrier to command buffer commandBuffer.pipelineBarrier( sourceStage, destinationStage, vk::DependencyFlagBits::eByRegion, @@ -1294,14 +1462,14 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima commandBuffer.end(); // Submit command buffer - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffer - }; - // Use mutex to ensure thread-safe access to graphics queue + // Use mutex to ensure thread-safe access to the graphics queue { - std::lock_guard lock(queueMutex); + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + std::lock_guard lock(queueMutex); graphicsQueue.submit(submitInfo, nullptr); graphicsQueue.waitIdle(); } @@ -1312,9 +1480,9 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima } // Copy buffer to image -void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { +void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector& regions) const { try { - // Create command buffer using RAII + // Create a command buffer using RAII vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, @@ -1331,27 +1499,12 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi commandBuffer.begin(beginInfo); - // Create buffer image copy region - vk::BufferImageCopy region{ - .bufferOffset = 0, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = { - .aspectMask = vk::ImageAspectFlagBits::eColor, - .mipLevel = 0, - .baseArrayLayer = 0, - .layerCount = 1 - }, - .imageOffset = {0, 0, 0}, - .imageExtent = {width, height, 1} - }; - - // Copy buffer to image + // Copy buffer to image using provided regions commandBuffer.copyBufferToImage( buffer, image, vk::ImageLayout::eTransferDstOptimal, - region + regions ); // End command buffer @@ -1374,3 +1527,188 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi throw; } } + +// Create or resize light storage buffers to accommodate the given number of lights +bool Renderer::createOrResizeLightStorageBuffers(size_t lightCount) { + try { + // Ensure we have storage buffers for each frame in flight + if (lightStorageBuffers.size() != MAX_FRAMES_IN_FLIGHT) { + lightStorageBuffers.resize(MAX_FRAMES_IN_FLIGHT); + } + + // Check if we need to resize buffers + bool needsResize = false; + for (auto& buffer : lightStorageBuffers) { + if (buffer.capacity < lightCount) { + needsResize = true; + break; + } + } + + if (!needsResize) { + return true; // Buffers are already large enough + } + + // Calculate new capacity (with some headroom for growth) + size_t newCapacity = std::max(lightCount * 2, size_t(64)); + vk::DeviceSize bufferSize = sizeof(LightData) * newCapacity; + + // Wait for device to be idle before destroying old buffers to prevent validation errors + // This ensures no GPU operations are using the old buffers + device.waitIdle(); + + // Create new buffers for each frame + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + auto& buffer = lightStorageBuffers[i]; + + // Clean up old buffer if it exists (now safe after waitIdle) + if (buffer.allocation) { + buffer.buffer = nullptr; + buffer.allocation.reset(); + buffer.mapped = nullptr; + } + + // Create new storage buffer + auto [newBuffer, newAllocation] = createBufferPooled( + bufferSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Get the mapped pointer from the allocation + void* mapped = newAllocation->mappedPtr; + + // Store the new buffer + buffer.buffer = std::move(newBuffer); + buffer.allocation = std::move(newAllocation); + buffer.mapped = mapped; + buffer.capacity = newCapacity; + buffer.size = 0; + } + + // Update all existing descriptor sets to reference the new light storage buffers + updateAllDescriptorSetsWithNewLightBuffers(); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create or resize light storage buffers: " << e.what() << std::endl; + return false; + } +} + +// Update all existing descriptor sets with new light storage buffer references +void Renderer::updateAllDescriptorSetsWithNewLightBuffers() { + try { + // Iterate through all entity resources and update their PBR descriptor sets + for (auto& [entity, resources] : entityResources) { + // Only update PBR descriptor sets (they have light buffer bindings) + if (!resources.pbrDescriptorSets.empty()) { + for (size_t i = 0; i < resources.pbrDescriptorSets.size() && i < lightStorageBuffers.size(); ++i) { + if (i < lightStorageBuffers.size() && *lightStorageBuffers[i].buffer) { + // Create descriptor write for light storage buffer (binding 7) + vk::DescriptorBufferInfo lightBufferInfo{ + .buffer = *lightStorageBuffers[i].buffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *resources.pbrDescriptorSets[i], + .dstBinding = 7, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightBufferInfo + }; + + // Update the descriptor set + device.updateDescriptorSets(descriptorWrite, {}); + } + } + } + } + } catch (const std::exception& e) { + std::cerr << "Failed to update descriptor sets with new light buffers: " << e.what() << std::endl; + } +} + +// Update the light storage buffer with current light data +bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector& lights) { + try { + // Ensure buffers are large enough and properly initialized + if (!createOrResizeLightStorageBuffers(lights.size())) { + return false; + } + + // Now check frame index after buffers are properly initialized + if (frameIndex >= lightStorageBuffers.size()) { + std::cerr << "Invalid frame index for light storage buffer update: " << frameIndex + << " >= " << lightStorageBuffers.size() << std::endl; + return false; + } + + auto& buffer = lightStorageBuffers[frameIndex]; + if (!buffer.mapped) { + std::cerr << "Light storage buffer not mapped" << std::endl; + return false; + } + + // Convert ExtractedLight data to LightData format + auto* lightData = static_cast(buffer.mapped); + for (size_t i = 0; i < lights.size(); ++i) { + const auto& light = lights[i]; + + // Set the light position + lightData[i].position = glm::vec4(light.position, 1.0f); + + // Set light color with proper intensity conversion + float intensity = light.intensity; + if (intensity < 1.0f) intensity = 1.0f; // Clamp weak intensities + + float lummenFromBlender = intensity * 1.0f / 683.0f; + + lightData[i].color = glm::vec4(light.color * lummenFromBlender, 1.0f); + + // Calculate light space matrix for shadow mapping + glm::mat4 lightProjection, lightView; + if (light.type == ExtractedLight::Type::Directional) { + float orthoSize = 50.0f; + lightProjection = glm::ortho(-orthoSize, orthoSize, -orthoSize, orthoSize, 0.1f, 100.0f); + lightView = glm::lookAt(light.position, light.position + light.direction, glm::vec3(0.0f, 1.0f, 0.0f)); + } else { + lightProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, light.range); + lightView = glm::lookAt(light.position, light.position + light.direction, glm::vec3(0.0f, 1.0f, 0.0f)); + } + lightData[i].lightSpaceMatrix = lightProjection * lightView; + + // Set light type + switch (light.type) { + case ExtractedLight::Type::Point: + lightData[i].lightType = 0; + break; + case ExtractedLight::Type::Directional: + lightData[i].lightType = 1; + break; + case ExtractedLight::Type::Spot: + lightData[i].lightType = 2; + break; + case ExtractedLight::Type::Emissive: + lightData[i].lightType = 3; + break; + } + + // Set other light properties + lightData[i].range = light.range; + lightData[i].innerConeAngle = light.innerConeAngle; + lightData[i].outerConeAngle = light.outerConeAngle; + } + + // Update buffer size + buffer.size = lights.size(); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to update light storage buffer: " << e.what() << std::endl; + return false; + } +} diff --git a/attachments/simple_engine/renderer_shadows.cpp b/attachments/simple_engine/renderer_shadows.cpp new file mode 100644 index 00000000..f1272257 --- /dev/null +++ b/attachments/simple_engine/renderer_shadows.cpp @@ -0,0 +1,140 @@ +#include "renderer.h" +#include "model_loader.h" +#include +#include + +// This file contains shadow mapping implementation for the Renderer class + +bool Renderer::createShadowMaps() { + try { + std::cout << "Creating shadow maps..." << std::endl; + + // Initialize shadow maps vector - limit to 16 for performance/memory + const uint32_t ACTUAL_SHADOW_MAPS = 16; + shadowMaps.resize(ACTUAL_SHADOW_MAPS); + + for (uint32_t i = 0; i < ACTUAL_SHADOW_MAPS; ++i) { + auto& shadowMap = shadowMaps[i]; + + // Create shadow map image using memory pool + auto [shadowImg, shadowImgAllocation] = createImagePooled( + DEFAULT_SHADOW_MAP_SIZE, + DEFAULT_SHADOW_MAP_SIZE, + vk::Format::eD32Sfloat, // 32-bit depth format for high precision + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + shadowMap.shadowMapImage = std::move(shadowImg); + shadowMap.shadowMapImageAllocation = std::move(shadowImgAllocation); + shadowMap.shadowMapSize = DEFAULT_SHADOW_MAP_SIZE; + + // Create shadow map image view + shadowMap.shadowMapImageView = createImageView( + shadowMap.shadowMapImage, + vk::Format::eD32Sfloat, + vk::ImageAspectFlagBits::eDepth + ); + + // Create shadow map sampler + vk::SamplerCreateInfo samplerInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eClampToBorder, + .addressModeV = vk::SamplerAddressMode::eClampToBorder, + .addressModeW = vk::SamplerAddressMode::eClampToBorder, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 1.0f, + .compareEnable = VK_TRUE, // Enable depth comparison for shadow mapping + .compareOp = vk::CompareOp::eLessOrEqual, + .minLod = 0.0f, + .maxLod = 1.0f, + .borderColor = vk::BorderColor::eFloatOpaqueWhite, // White border = no shadow + .unnormalizedCoordinates = VK_FALSE + }; + + shadowMap.shadowMapSampler = vk::raii::Sampler(device, samplerInfo); + + // Transition shadow map to read-only layout for shader sampling + // Shadow maps will be transitioned to attachment layout when rendering, then back to read-only + transitionImageLayout( + *shadowMap.shadowMapImage, + vk::Format::eD32Sfloat, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eDepthStencilReadOnlyOptimal + ); + + std::cout << " Created shadow map " << i << " (" << DEFAULT_SHADOW_MAP_SIZE << "x" << DEFAULT_SHADOW_MAP_SIZE << ")" << std::endl; + } + + std::cout << "Successfully created " << ACTUAL_SHADOW_MAPS << " shadow maps" << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Failed to create shadow maps: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::createShadowMapRenderPass() { + try { + std::cout << "Creating shadow map render pass..." << std::endl; + + // We'll use dynamic rendering instead of traditional render passes + // This is more flexible and matches our existing rendering approach + + std::cout << "Shadow map render pass created (using dynamic rendering)" << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Failed to create shadow map render pass: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::createShadowMapFramebuffers() { + try { + std::cout << "Creating shadow map framebuffers..." << std::endl; + + // With dynamic rendering, we don't need traditional framebuffers + // The shadow map images will be used directly in dynamic rendering + + std::cout << "Shadow map framebuffers created (using dynamic rendering)" << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Failed to create shadow map framebuffers: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::createShadowMapDescriptorSetLayout() { + try { + std::cout << "Creating shadow map descriptor set layout..." << std::endl; + + // We need to update the existing PBR descriptor set layout to include shadow maps + // This will be done by modifying the createPBRDescriptorSetLayout method + + std::cout << "Shadow map descriptor set layout will be integrated with PBR layout" << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Failed to create shadow map descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +void Renderer::renderShadowMaps(const std::vector& entities, const std::vector& lights) { + // This method will render the scene from each light's perspective to generate shadow maps + // Implementation will be added after basic shadow map creation is working + std::cout << "Shadow map rendering not yet implemented" << std::endl; +} + +void Renderer::updateShadowMapUniforms(uint32_t lightIndex, const ExtractedLight& light) { + // This method will calculate and update the light space matrix for a specific light + // Implementation will be added after basic shadow map creation is working + std::cout << "Shadow map uniform updates not yet implemented" << std::endl; +} diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 0d94a101..3d769648 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -2,127 +2,295 @@ #include "engine.h" #include "transform_component.h" #include "mesh_component.h" +#include "camera_component.h" #include -#include - -// Global loading state definition -LoadingState g_loadingState; +#include +#include +#include /** - * @brief Background thread function to load the Bistro model. - * @param modelLoader Pointer to the model loader. + * @brief Calculate bounding box dimensions for a MaterialMesh. + * @param materialMesh The MaterialMesh to analyze. + * @return The size of the bounding box (max - min for each axis). */ -void LoadBistroModelAsync(ModelLoader* modelLoader) { - try { - std::cout << "Starting thread-safe background loading of Bistro model..." << std::endl; - g_loadingState.isLoading = true; - g_loadingState.loadingComplete = false; - g_loadingState.loadingFailed = false; - - // Parse GLTF data without creating Vulkan resources (thread-safe) - std::vector materialMeshes = modelLoader->ParseGLTFDataOnly("../Assets/Bistro.glb"); - if (materialMeshes.empty()) { - g_loadingState.errorMessage = "Failed to parse Bistro.glb - no material meshes found"; - g_loadingState.loadingFailed = true; - g_loadingState.isLoading = false; - return; - } - - g_loadingState.totalMaterials = static_cast(materialMeshes.size()); +glm::vec3 CalculateBoundingBoxSize(const MaterialMesh& materialMesh) { + if (materialMesh.vertices.empty()) { + return glm::vec3(0.0f); + } - std::cout << "Parsed " << materialMeshes.size() << " materials in background thread (thread-safe)" << std::endl; + glm::vec3 minBounds = materialMesh.vertices[0].position; + glm::vec3 maxBounds = materialMesh.vertices[0].position; - // Store the loaded materials (thread-safe copy) - { - std::lock_guard lock(g_loadingState.entityCreationMutex); - g_loadingState.loadedMaterials = std::move(materialMeshes); - } + for (const auto& vertex : materialMesh.vertices) { + minBounds = glm::min(minBounds, vertex.position); + maxBounds = glm::max(maxBounds, vertex.position); + } - g_loadingState.loadingComplete = true; - g_loadingState.isLoading = false; - std::cout << "Thread-safe background loading completed successfully!" << std::endl; + return maxBounds - minBounds; +} - } catch (const std::exception& e) { - g_loadingState.errorMessage = std::string("Thread-safe loading error: ") + e.what(); - g_loadingState.loadingFailed = true; - g_loadingState.isLoading = false; - std::cerr << "Thread-safe background loading failed: " << e.what() << std::endl; - } +/** + * @brief Determine if an object is considered "small" based on its bounding box. + * @param boundingBoxSize The size of the bounding box. + * @return True if the object is small, false otherwise. + */ +bool IsSmallObject(const glm::vec3& boundingBoxSize) { + // Consider an object "small" if its largest dimension is less than 8.0 units + float maxDimension = std::max({boundingBoxSize.x, boundingBoxSize.y, boundingBoxSize.z}); + return maxDimension < 0.1f; } /** - * @brief Create entities from loaded materials (called from main thread). + * @brief Load a GLTF model synchronously on the main thread. * @param engine The engine to create entities in. + * @param modelPath The path to the GLTF model file. + * @param position The position to place the model (default: origin with slight Y offset). + * @param rotation The rotation to apply to the model (default: no rotation). + * @param scale The scale to apply to the model (default: unit scale). */ -void CreateEntitiesFromLoadedMaterials(Engine* engine) { - std::lock_guard lock(g_loadingState.entityCreationMutex); +void LoadGLTFModel(Engine* engine, const std::string& modelPath, + const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale) { + std::cout << "Loading GLTF model synchronously on main thread: " << modelPath << std::endl; - if (g_loadingState.loadedMaterials.empty()) { + // Get the model loader and renderer + ModelLoader* modelLoader = engine->GetModelLoader(); + Renderer* renderer = engine->GetRenderer(); + + if (!modelLoader || !renderer) { + std::cerr << "Error: ModelLoader or Renderer is null" << std::endl; return; } - std::cout << "Creating " << g_loadingState.loadedMaterials.size() << " entities from loaded materials..." << std::endl; + // Extract model name from file path for entity naming + std::filesystem::path modelFilePath(modelPath); + std::string modelName = modelFilePath.stem().string(); // Get filename without extension - // Get the model loader and renderer for texture loading on main thread - ModelLoader* modelLoader = engine->GetModelLoader(); - Renderer* renderer = engine->GetRenderer(); + try { + // Load the complete GLTF model with all textures and lighting on the main thread + Model* loadedModel = modelLoader->LoadGLTF(modelPath); + if (!loadedModel) { + std::cerr << "Failed to load GLTF model: " << modelPath << std::endl; + return; + } + + std::cout << "Successfully loaded GLTF model with all textures and lighting: " << modelPath << std::endl; + + // Extract lights from the model and transform them to world space + std::vector extractedLights = modelLoader->GetExtractedLights(modelPath); + + // Create transformation matrix from position, rotation, and scale + glm::mat4 transformMatrix = glm::mat4(1.0f); + transformMatrix = glm::translate(transformMatrix, position); + transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + transformMatrix = glm::scale(transformMatrix, scale); + + // Transform all light positions from local model space to world space + for (auto& light : extractedLights) { + glm::vec4 worldPos = transformMatrix * glm::vec4(light.position, 1.0f); + light.position = glm::vec3(worldPos); + + // Also transform the light direction (for directional lights) + glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(transformMatrix))); + light.direction = glm::normalize(normalMatrix * light.direction); + } + + renderer->SetStaticLights(extractedLights); + + // Extract and apply cameras from the GLTF model + const std::vector& cameras = loadedModel->GetCameras(); + if (!cameras.empty()) { + std::cout << "Found " << cameras.size() << " camera(s) in GLTF model, using the first one to replace default camera" << std::endl; + + const CameraData& gltfCamera = cameras[0]; // Use the first camera + + // Find or create a camera entity to replace the default one + Entity* cameraEntity = engine->GetEntity("Camera"); + if (!cameraEntity) { + // Create a new camera entity if none exists + cameraEntity = engine->CreateEntity("Camera"); + if (cameraEntity) { + cameraEntity->AddComponent(); + cameraEntity->AddComponent(); + } + } + + if (cameraEntity) { + // Update the camera transform with GLTF data + auto* cameraTransform = cameraEntity->GetComponent(); + if (cameraTransform) { + // Apply the transformation matrix to the camera position + glm::vec4 worldPos = transformMatrix * glm::vec4(gltfCamera.position, 1.0f); + cameraTransform->SetPosition(glm::vec3(worldPos)); + + // Apply rotation from GLTF camera + glm::vec3 eulerAngles = glm::eulerAngles(gltfCamera.rotation); + cameraTransform->SetRotation(glm::degrees(eulerAngles)); + + std::cout << " Applied GLTF camera position: (" << worldPos.x << ", " << worldPos.y << ", " << worldPos.z << ")" << std::endl; + std::cout << " Applied GLTF camera rotation: (" << glm::degrees(eulerAngles.x) << ", " << glm::degrees(eulerAngles.y) << ", " << glm::degrees(eulerAngles.z) << ")" << std::endl; + } - // First, load the actual GLTF model with Vulkan resources on the main thread - // This will create all the textures that the background thread couldn't create - if (modelLoader && renderer) { - std::cout << "Loading Bistro model with Vulkan resources on main thread..." << std::endl; - Model* bistroModel = modelLoader->LoadGLTF("../Assets/Bistro.glb"); - if (bistroModel) { - std::cout << "Successfully loaded Bistro model with Vulkan resources on main thread" << std::endl; + // Update the camera component with GLTF properties + auto* camera = cameraEntity->GetComponent(); + if (camera) { + if (gltfCamera.isPerspective) { + camera->SetFieldOfView(glm::degrees(gltfCamera.fov)); // Convert radians to degrees + camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); + if (gltfCamera.aspectRatio > 0.0f) { + camera->SetAspectRatio(gltfCamera.aspectRatio); + } + std::cout << " Applied GLTF perspective camera: FOV=" << glm::degrees(gltfCamera.fov) + << ", Near=" << gltfCamera.nearPlane << ", Far=" << gltfCamera.farPlane << std::endl; + } else { + // Handle orthographic camera if needed + camera->SetProjectionType(CameraComponent::ProjectionType::Orthographic); + camera->SetOrthographicSize(gltfCamera.orthographicSize, gltfCamera.orthographicSize); + camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); + std::cout << " Applied GLTF orthographic camera: Size=" << gltfCamera.orthographicSize + << ", Near=" << gltfCamera.nearPlane << ", Far=" << gltfCamera.farPlane << std::endl; + } + + // Set this as the active camera + engine->SetActiveCamera(camera); + std::cout << " Set GLTF camera as active camera" << std::endl; + } + } } else { - std::cerr << "Warning: Failed to load Bistro model with Vulkan resources on main thread" << std::endl; + std::cout << "No cameras found in GLTF model, keeping default camera" << std::endl; } - } - int entitiesCreated = 0; - for (const auto& materialMesh : g_loadingState.loadedMaterials) { - // Create entity name based on material - std::string entityName = "Bistro_Material_" + std::to_string(materialMesh.materialIndex) + - "_" + materialMesh.materialName; - - if (Entity* materialEntity = engine->CreateEntity(entityName)) { - // Add transform component - auto* transform = materialEntity->AddComponent(); - transform->SetPosition(glm::vec3(0.0f, -1.5f, 0.0f)); - transform->SetRotation(glm::vec3(0.0f, 0.0f, 0.0f)); - transform->SetScale(glm::vec3(1.0f, 1.0f, 1.0f)); - - // Add mesh component with material-specific data - auto* mesh = materialEntity->AddComponent(); - mesh->SetVertices(materialMesh.vertices); - mesh->SetIndices(materialMesh.indices); - - // Set the correct texture for this material - // The texture should now be loaded since we called LoadGLTF on the main thread + // Get the material meshes from the loaded model + const std::vector& materialMeshes = modelLoader->GetMaterialMeshes(modelPath); + if (materialMeshes.empty()) { + std::cerr << "No material meshes found in loaded model: " << modelPath << std::endl; + return; + } + + std::cout << "Creating " << materialMeshes.size() << " entities from loaded materials..." << std::endl; + + // First, collect and load all unique external texture files + std::set uniqueTextures; + for (const auto& materialMesh : materialMeshes) { + // Collect all texture types from this material + if (!materialMesh.baseColorTexturePath.empty()) { + uniqueTextures.insert(materialMesh.baseColorTexturePath); + } + if (!materialMesh.normalTexturePath.empty()) { + uniqueTextures.insert(materialMesh.normalTexturePath); + } + if (!materialMesh.metallicRoughnessTexturePath.empty()) { + uniqueTextures.insert(materialMesh.metallicRoughnessTexturePath); + } + if (!materialMesh.occlusionTexturePath.empty()) { + uniqueTextures.insert(materialMesh.occlusionTexturePath); + } + if (!materialMesh.emissiveTexturePath.empty()) { + uniqueTextures.insert(materialMesh.emissiveTexturePath); + } + // Also include legacy texturePath for backward compatibility if (!materialMesh.texturePath.empty()) { - mesh->SetTexturePath(materialMesh.texturePath); - std::cout << " Entity " << entityName << ": " << materialMesh.vertices.size() - << " vertices, texture: " << materialMesh.texturePath << std::endl; - } else { - std::cout << " Entity " << entityName << ": " << materialMesh.vertices.size() - << " vertices, no texture" << std::endl; + uniqueTextures.insert(materialMesh.texturePath); } + } - // Pre-allocate all Vulkan resources for this Bistro entity - if (!renderer->preAllocateEntityResources(materialEntity)) { - std::cerr << "Failed to pre-allocate resources for Bistro entity: " << entityName << std::endl; - // Continue with other entities even if one fails + // Filter out embedded GLTF textures (already loaded in memory) and load only actual external files + std::set externalTextures; + for (const std::string& texturePath : uniqueTextures) { + // Skip embedded GLTF textures (they start with "gltf_texture_" and are already loaded in memory) + if (texturePath.find("gltf_texture_") != 0) { + externalTextures.insert(texturePath); } + } - entitiesCreated++; - g_loadingState.materialsLoaded = entitiesCreated; + if (!externalTextures.empty()) { + std::cout << "Loading " << externalTextures.size() << " unique external texture files..." << std::endl; + for (const std::string& texturePath : externalTextures) { + if (!renderer->LoadTexture(texturePath)) { + std::cerr << "Warning: Failed to load external texture: " << texturePath << std::endl; + } + } } else { - std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; + std::cout << "No external texture files to load (all textures are embedded in GLTF)" << std::endl; } - } - std::cout << "Successfully created " << entitiesCreated << " entities from loaded materials" << std::endl; + int entitiesCreated = 0; + int smallObjectsCreated = 0; + for (const auto& materialMesh : materialMeshes) { + // Calculate bounding box size to determine if this is a small object + glm::vec3 boundingBoxSize = CalculateBoundingBoxSize(materialMesh); + bool isSmall = IsSmallObject(boundingBoxSize); - // Clear the loaded materials to free memory - g_loadingState.loadedMaterials.clear(); + // Create an entity name based on model and material, with special marking for small objects + std::string entityName = modelName + "_Material_" + std::to_string(materialMesh.materialIndex) + + "_" + materialMesh.materialName; + if (isSmall) { + entityName += "_SMALL_POKEABLE"; + smallObjectsCreated++; + } + + if (Entity* materialEntity = engine->CreateEntity(entityName)) { + // Add a transform component with provided parameters + auto* transform = materialEntity->AddComponent(); + transform->SetPosition(position); + transform->SetRotation(rotation); + transform->SetScale(scale); + + // Add a mesh component with material-specific data + auto* mesh = materialEntity->AddComponent(); + mesh->SetVertices(materialMesh.vertices); + mesh->SetIndices(materialMesh.indices); + + // Set ALL PBR texture paths for this material + // Set primary texture path for backward compatibility + if (!materialMesh.texturePath.empty()) { + mesh->SetTexturePath(materialMesh.texturePath); + } + + // Set all PBR texture paths + if (!materialMesh.baseColorTexturePath.empty()) { + mesh->SetBaseColorTexturePath(materialMesh.baseColorTexturePath); + } + if (!materialMesh.normalTexturePath.empty()) { + mesh->SetNormalTexturePath(materialMesh.normalTexturePath); + } + if (!materialMesh.metallicRoughnessTexturePath.empty()) { + mesh->SetMetallicRoughnessTexturePath(materialMesh.metallicRoughnessTexturePath); + } + if (!materialMesh.occlusionTexturePath.empty()) { + mesh->SetOcclusionTexturePath(materialMesh.occlusionTexturePath); + } + if (!materialMesh.emissiveTexturePath.empty()) { + mesh->SetEmissiveTexturePath(materialMesh.emissiveTexturePath); + } + + // Pre-allocate all Vulkan resources for this entity + if (!renderer->preAllocateEntityResources(materialEntity)) { + std::cerr << "Failed to pre-allocate resources for entity: " << entityName << std::endl; + // Continue with other entities even if one fails + } + + entitiesCreated++; + } else { + std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; + } + } + + std::cout << "Successfully created " << entitiesCreated << " entities from loaded materials" << std::endl; + std::cout << " - " << smallObjectsCreated << " small pokeable objects identified" << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Error loading GLTF model: " << e.what() << std::endl; + } +} + +/** + * @brief Load a GLTF model with default transform values. + * @param engine The engine to create entities in. + * @param modelPath The path to the GLTF model file. + */ +void LoadGLTFModel(Engine* engine, const std::string& modelPath) { + // Use default transform values: slight Y offset, no rotation, unit scale + LoadGLTFModel(engine, modelPath, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.0f, 1.0f)); } diff --git a/attachments/simple_engine/scene_loading.h b/attachments/simple_engine/scene_loading.h index e4ec5800..332fdfa2 100644 --- a/attachments/simple_engine/scene_loading.h +++ b/attachments/simple_engine/scene_loading.h @@ -1,38 +1,28 @@ #pragma once -#include -#include #include #include +#include #include "model_loader.h" // Forward declarations class Engine; class ModelLoader; -// Structure to track threaded loading state -struct LoadingState { - std::atomic isLoading{false}; - std::atomic loadingComplete{false}; - std::atomic loadingFailed{false}; - std::atomic materialsLoaded{0}; - std::atomic totalMaterials{0}; - std::mutex entityCreationMutex; - std::vector loadedMaterials; - std::string errorMessage; -}; - -// Global loading state -extern LoadingState g_loadingState; - /** - * @brief Background thread function to load the Bistro model. - * @param modelLoader Pointer to the model loader. + * @brief Load a GLTF model synchronously on the main thread. + * @param engine The engine to create entities in. + * @param modelPath The path to the GLTF model file. + * @param position The position to place the model. + * @param rotation The rotation to apply to the model. + * @param scale The scale to apply to the model. */ -void LoadBistroModelAsync(ModelLoader* modelLoader); +void LoadGLTFModel(Engine* engine, const std::string& modelPath, + const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale); /** - * @brief Create entities from loaded materials (called from main thread). + * @brief Load a GLTF model with default transform values. * @param engine The engine to create entities in. + * @param modelPath The path to the GLTF model file. */ -void CreateEntitiesFromLoadedMaterials(Engine* engine); +void LoadGLTFModel(Engine* engine, const std::string& modelPath); diff --git a/attachments/simple_engine/shaders/lighting.slang b/attachments/simple_engine/shaders/lighting.slang index b72ab400..4837f3d5 100644 --- a/attachments/simple_engine/shaders/lighting.slang +++ b/attachments/simple_engine/shaders/lighting.slang @@ -1,11 +1,13 @@ -// Combined vertex and fragment shader for basic lighting -// This shader implements the Phong lighting model with push constants for material properties +// Combined vertex and fragment shader for basic/legacy lighting +// This shader implements the Phong lighting model as a fallback when BRDF/PBR is disabled +// Note: BRDF/PBR is now the default lighting model - this is used only when explicitly requested // Input from vertex buffer struct VSInput { float3 Position : POSITION; float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Added to match vertex layout (unused in basic lighting) }; // Output from vertex shader / Input to fragment shader @@ -14,6 +16,7 @@ struct VSOutput { float3 WorldPos : POSITION; float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Pass through tangent (unused in basic lighting) }; // Uniform buffer for transformation matrices and light information @@ -60,6 +63,9 @@ VSOutput VSMain(VSInput input) // Pass texture coordinates output.TexCoord = input.TexCoord; + // Pass tangent (unused in basic lighting but required for vertex layout compatibility) + output.Tangent = input.Tangent; + return output; } diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index b86244a6..70788717 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -17,18 +17,31 @@ struct VSOutput { float4 Tangent : TANGENT; }; -// Uniform buffer +// Light data structure for storage buffer +struct LightData { + float4 position; // Light position (w component unused) + float4 color; // Light color and intensity + float4x4 lightSpaceMatrix; // Light space matrix for shadow mapping + int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive + float range; // Light range + float innerConeAngle; // For spot lights + float outerConeAngle; // For spot lights +}; + +// Uniform buffer (now without fixed light arrays) struct UniformBufferObject { float4x4 model; float4x4 view; float4x4 proj; - float4 lightPositions[4]; - float4 lightColors[4]; float4 camPos; float exposure; float gamma; float prefilteredCubeMipLevels; float scaleIBLAmbient; + int lightCount; // Number of active lights (dynamic) + int shadowMapCount; // Number of active shadow maps (dynamic) + float shadowBias; // Shadow bias to prevent shadow acne + float padding; // Padding for alignment }; // Push constants for material properties @@ -43,6 +56,8 @@ struct PushConstants { int emissiveTextureSet; float alphaMask; float alphaMaskCutoff; + float3 emissiveFactor; // Emissive factor for HDR emissive sources + float emissiveStrength; // KHR_materials_emissive_strength extension }; // Constants @@ -55,6 +70,8 @@ static const float PI = 3.14159265359; [[vk::binding(3, 0)]] Sampler2D normalMap; [[vk::binding(4, 0)]] Sampler2D occlusionMap; [[vk::binding(5, 0)]] Sampler2D emissiveMap; +[[vk::binding(6, 0)]] Sampler2D shadowMaps[16]; // Array of individual shadow maps for lights +[[vk::binding(7, 0)]] StructuredBuffer lightBuffer; // Dynamic light storage buffer [[vk::push_constant]] PushConstants material; @@ -85,6 +102,49 @@ float3 FresnelSchlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } +// Shadow mapping functions +float calculateShadow(float4 fragPosLightSpace, int shadowMapIndex, float3 normal, float3 lightDir) { + // Add bounds checking to prevent accessing invalid shadow map indices + if (shadowMapIndex < 0 || shadowMapIndex >= 16) { + return 0.0; // No shadow for invalid indices + } + + // Perform perspective divide + float3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; + + // Transform to [0,1] range + projCoords = projCoords * 0.5 + 0.5; + + // Check if fragment is outside light's frustum + if (projCoords.z > 1.0 || projCoords.x < 0.0 || projCoords.x > 1.0 || + projCoords.y < 0.0 || projCoords.y > 1.0) { + return 0.0; // No shadow outside frustum + } + + // Calculate bias to prevent shadow acne + float bias = max(ubo.shadowBias * (1.0 - dot(normal, lightDir)), ubo.shadowBias * 0.1); + + // Get closest depth value from light's perspective + float closestDepth = shadowMaps[shadowMapIndex].Sample(projCoords.xy).r; + + // Get depth of current fragment from light's perspective + float currentDepth = projCoords.z; + + // PCF (Percentage Closer Filtering) for smooth shadows + float shadow = 0.0; + float2 texelSize = 1.0 / float2(2048.0, 2048.0); // Shadow map resolution + + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + float pcfDepth = shadowMaps[shadowMapIndex].Sample(projCoords.xy + float2(x, y) * texelSize).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; // Average the 9 samples + + return shadow; +} + // Vertex shader entry point [[shader("vertex")]] VSOutput VSMain(VSInput input) @@ -120,7 +180,7 @@ float4 PSMain(VSOutput input) : SV_TARGET float metallic = metallicRoughness.x * material.metallicFactor; float roughness = metallicRoughness.y * material.roughnessFactor; float ao = occlusionMap.Sample(input.UV).r; - float3 emissive = emissiveMap.Sample(input.UV).rgb; + float3 emissive = emissiveMap.Sample(input.UV).rgb * material.emissiveFactor * material.emissiveStrength; // Calculate normal in tangent space float3 N = normalize(input.Normal); @@ -144,50 +204,116 @@ float4 PSMain(VSOutput input) : SV_TARGET // Initialize lighting float3 Lo = float3(0.0, 0.0, 0.0); - // Calculate lighting for each light - for (int i = 0; i < 4; i++) { - float3 lightPos = ubo.lightPositions[i].xyz; - float3 lightColor = ubo.lightColors[i].rgb; - - // Calculate light direction and distance - float3 L = normalize(lightPos - input.WorldPos); - float distance = length(lightPos - input.WorldPos); - float attenuation = 1.0 / (distance * distance); - float3 radiance = lightColor * attenuation; - - // Calculate half vector - float3 H = normalize(V + L); - - // Calculate BRDF terms - float NdotL = max(dot(N, L), 0.0); - float NdotV = max(dot(N, V), 0.0); - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); - - // Specular BRDF - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmith(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(HdotV, F0); - - float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; - float3 specular = numerator / denominator; - - // Energy conservation - float3 kS = F; - float3 kD = float3(1.0, 1.0, 1.0) - kS; - kD *= 1.0 - metallic; - - // Add to outgoing radiance - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + // Calculate lighting for each light (dynamic count - no limit) + for (int i = 0; i < ubo.lightCount; i++) { + LightData light = lightBuffer[i]; + float3 lightPos = light.position.xyz; + float3 lightColor = light.color.rgb; + + // Handle emissive lights differently + if (light.lightType == 3) { // Emissive light + // For emissive lights, treat them as area lights with enhanced contribution + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + + // Enhanced attenuation for emissive lights to make them more effective + float attenuation = 1.0 / (1.0 + 0.1 * distance + 0.01 * distance * distance); + + // Boost emissive light intensity to make them more visible + float3 radiance = lightColor * attenuation * 2.0; // 2x multiplier for emissive lights + + // Calculate half vector + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Calculate shadow factor for emissive lights + float shadow = 0.0; + if (i < ubo.shadowMapCount) { + // Transform fragment position to light space + float4 fragPosLightSpace = mul(light.lightSpaceMatrix, float4(input.WorldPos, 1.0)); + shadow = calculateShadow(fragPosLightSpace, i, N, L); + } + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // Energy conservation + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Apply shadow factor to lighting + float shadowFactor = 1.0 - shadow; + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL * shadowFactor; + + } else { // Regular lights (Point, Directional, Spot) + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + // Calculate half vector + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Calculate shadow factor + float shadow = 0.0; + if (i < ubo.shadowMapCount) { + // Transform fragment position to light space + float4 fragPosLightSpace = mul(light.lightSpaceMatrix, float4(input.WorldPos, 1.0)); + shadow = calculateShadow(fragPosLightSpace, i, N, L); + } + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // Energy conservation + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Apply shadow factor to lighting (1.0 - shadow means 0 shadow = full light, 1 shadow = no light) + float shadowFactor = 1.0 - shadow; + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL * shadowFactor; + } } // Add only emissive (no hardcoded ambient - use only model-defined lights) - // Scale down emissive contribution to reduce excessive brightness - float3 color = Lo + emissive * 0.1; + // Use full emissive contribution for proper HDR rendering + float3 color = Lo + emissive; + + // Apply exposure before tone mapping for proper HDR workflow + color *= ubo.exposure; + + // Standard Reinhard tone mapping - simple and effective + // This prevents excessive brightness while preserving color relationships + color = color / (1.0 + color); - // HDR tonemapping and gamma correction - color = color / (color + float3(1.0, 1.0, 1.0)); + // Gamma correction (convert from linear to sRGB) color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); return float4(color, baseColor.a); diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang index 20457330..9c0b0373 100644 --- a/attachments/simple_engine/shaders/texturedMesh.slang +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -15,6 +15,7 @@ struct VSOutput { float3 WorldPos; float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Pass through tangent to satisfy validation layer }; // Uniform buffer @@ -42,6 +43,7 @@ VSOutput VSMain(VSInput input) output.WorldPos = worldPos.xyz; output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); output.TexCoord = input.TexCoord; + output.Tangent = input.Tangent; // Pass through tangent (unused in basic rendering) return output; } From be45542bcaa8e490a1d96ea240f93907caba762a Mon Sep 17 00:00:00 2001 From: Steven Winston Date: Tue, 29 Jul 2025 16:01:18 -0700 Subject: [PATCH 034/102] Update 04_rendering_approaches.adoc --- .../Mobile_Development/04_rendering_approaches.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc index cf9b8029..98b0eb2f 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -110,7 +110,7 @@ vk::RenderPass render_pass = device.createRenderPass(render_pass_info); * *Optimize for Tile Size*: Consider the tile size when designing your rendering algorithm. For example, if you know the tile size is 16x16, you -might organize your data or algorithms to work efficiently with that size.Ok +might organize your data or algorithms to work efficiently with that size. ===== Memory Management From 8bdf3d153fcaea13f559949733095998803edce2 Mon Sep 17 00:00:00 2001 From: Steven Winston Date: Wed, 30 Jul 2025 00:13:53 -0700 Subject: [PATCH 035/102] Update 04_rendering_approaches.adoc --- .../Mobile_Development/04_rendering_approaches.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc index 98b0eb2f..d38d1ea8 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -183,9 +183,9 @@ Proper depth testing is crucial for TBR performance: * *Avoid Operations That Disable Early-Z*: The following operations can prevent effective early depth testing: - Using the discard instruction in fragment shaders - - Writing to gl_FragDepth explicitly + - Writing to gl_FragDepth (GLSL) SV_Depth (slang) explicitly - Using storage images or storage buffers - - Using gl_SampleMask + - Using gl_SampleMask (GLSL explicit way to turn on/off specific pixels) - Enabling both depth bounds and depth write - Enabling both blending and depth write From e80fd231da09bc003deaaa43178f62ef1762cb94 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 30 Jul 2025 15:13:12 -0700 Subject: [PATCH 036/102] address comments --- en/01_Overview.adoc | 4 ++-- en/02_Development_environment.adoc | 18 +++++++++--------- .../02_architectural_patterns.adoc | 4 ++-- .../Lighting_Materials/01_introduction.adoc | 5 +++++ 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/en/01_Overview.adoc b/en/01_Overview.adoc index f3f394be..33493847 100644 --- a/en/01_Overview.adoc +++ b/en/01_Overview.adoc @@ -28,7 +28,7 @@ with the existing APIs somehow. This resulted in less than ideal abstractions Aside from these new features, the past decade also saw an influx of mobile and embedded devices with powerful graphics hardware. These mobile GPUs have different architectures based on their energy and space requirements. -One such example is https://en.wikipedia.org/wiki/Tiled_rendering[tiled rendering], +One such example is https://en.wikipedia.org/wiki/Tiled_rendering[tiled rendering], which would benefit from improved performance by offering the programmer more control over this functionality. Another limitation originating from the age of these APIs is limited @@ -320,7 +320,7 @@ _validation layers_. Validation layers are pieces of code that can be inserted between the API and the graphics driver to do things like running extra checks on function parameters and tracking memory management problems. -The nice thing is that you can enable them during development and then +An important benefit is that you can enable them during development and then completely disable them when releasing your application for zero overhead. Anyone can write their own validation layers, but the Vulkan SDK by LunarG provides a standard set of validation layers that we'll be using in this tutorial. diff --git a/en/02_Development_environment.adoc b/en/02_Development_environment.adoc index 5cf549db..f9efc09a 100644 --- a/en/02_Development_environment.adoc +++ b/en/02_Development_environment.adoc @@ -65,9 +65,9 @@ contains the libraries. Lastly, there's the `include` directory that contains the Vulkan headers. Feel free to explore the other files, but we won't need them for this tutorial. -To automatically set the environment variables up that VulkanSDK will use to -make life easier with the CMake project configuration and various other -tooling, We recommend using the `setup-env` script. This can be added to +To automatically set the environment variables that VulkanSDK will use, we +recommend using the `setup-env` script. This makes life easier with the CMake +project configuration and various other tooling. The script can be added to your auto-start for your terminal and IDE setup such that those environment variables work everywhere. @@ -154,9 +154,9 @@ target_sources(VulkanCppModule ) ---- -The VulkanCppModule target only needs to be defined once, then add it to the -dependency of your consuming project, and it will be built automatically, and -you won't need to also add Vulkan::Vulkan to your project. +The VulkanCppModule target only needs to be defined once. Then add it to the +dependency of your consuming project, and it will be built automatically. +You won't need to also add Vulkan::Vulkan to your project. [,cmake] ---- @@ -245,7 +245,7 @@ GLFW on the https://www.glfw.org/download.html[official website]. In this tutorial, we'll be using the 64-bit binaries, but you can of course also choose to build in 32-bit mode. In that case make sure to link with the Vulkan SDK binaries in the `Lib32` directory instead of `Lib`. After downloading it, extract the archive -to a convenient location. I've chosen to create a `Libraries` directory in the +to a convenient location. We've chosen to create a `Libraries` directory in the Visual Studio directory under documents. image::/images/glfw_directory.png[] @@ -272,7 +272,7 @@ Now that you have installed all the dependencies, we can set up a basic CMake project for Vulkan and write a little bit of code to make sure that everything works. -I will assume that you already have some basic experience with CMake, like +We will assume that you already have some basic experience with CMake, like how variables and rules work. If not, you can get up to speed very quickly with https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/tutorial/[this tutorial]. You can now use the link:/attachments/[attachments] directory in this tutorial @@ -410,7 +410,7 @@ Now that you have installed all the dependencies, we can set up a basic CMake project for Vulkan and write a little bit of code to make sure that everything works. -I will assume that you already have some basic experience with CMake, like +We will assume that you already have some basic experience with CMake, like how variables and rules work. If not, you can get up to speed very quickly with https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/tutorial/[this tutorial]. You can now use the link:/attachments/[attachments] directory in this tutorial as a template for your diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index 9d33745a..e0d595d6 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -70,8 +70,8 @@ image::../../../images/component_based_architecture_diagram.svg[Component-Based * *Boxes*: Blue boxes represent Entities, orange boxes represent Components, and green boxes represent Systems * *Line Types*: - ** Dashed lines show ownership/containment (Entities contain Components) - ** Solid lines show processing relationships (Systems process specific Components) +** Dashed lines show ownership/containment (Entities contain Components) +** Solid lines show processing relationships (Systems process specific Components) * *Text*: All text elements use dark colors for visibility in both light and dark modes * *Directional Flow*: Arrows indicate the direction of relationships between elements ==== diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc index e6461d3d..79ac7435 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc @@ -2,6 +2,11 @@ In this chapter, we'll explore the fundamentals of lighting and materials in 3D rendering, with a focus on Physically Based Rendering (PBR). Lighting is a crucial aspect of creating realistic and visually appealing 3D scenes. Without proper lighting, even the most detailed models can appear flat and lifeless. +[NOTE] +==== +*About PBR References*: Throughout this tutorial, you may encounter references to PBR (Physically Based Rendering) before reaching this chapter. PBR is a modern rendering approach that simulates how light interacts with surfaces based on physical principles. We'll cover PBR in detail in the sections that follow, so don't worry if you're not familiar with these concepts yet. +==== + This chapter serves as the foundation for understanding how light interacts with different materials in a physically accurate way. The concepts you'll learn here will be applied in later chapters, including the Loading_Models chapter where we'll use this knowledge to render glTF models with PBR materials. Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. From d834c39c8672741620623df126a01c817e8c580b Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 30 Jul 2025 17:48:10 -0700 Subject: [PATCH 037/102] address comments --- .../02_architectural_patterns.adoc | 2 +- .../05_rendering_pipeline.adoc | 2 +- .../component_based_architecture_diagram.png | Bin 0 -> 27752 bytes .../component_based_architecture_diagram.svg | 118 ---------------- images/rendering_pipeline_flowchart.png | Bin 0 -> 38975 bytes images/rendering_pipeline_flowchart.svg | 128 ------------------ 6 files changed, 2 insertions(+), 248 deletions(-) create mode 100644 images/component_based_architecture_diagram.png delete mode 100644 images/component_based_architecture_diagram.svg create mode 100644 images/rendering_pipeline_flowchart.png delete mode 100644 images/rendering_pipeline_flowchart.svg diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index e0d595d6..ea89d335 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -62,7 +62,7 @@ For detailed information and implementation examples, see the link:../Appendix/a Component-based architecture is widely used in modern game engines and forms the foundation of our Vulkan rendering engine. It promotes composition over inheritance and allows for more flexible entity design. -image::../../../images/component_based_architecture_diagram.svg[Component-Based Architecture Diagram, width=600, alt="Component-Based Architecture Diagram showing entities, components, and systems"] +image::../../../images/component_based_architecture_diagram.png[Component-Based Architecture Diagram, width=600, alt="Component-Based Architecture Diagram showing entities, components, and systems"] [NOTE] ==== diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index e9b09266..a41184d7 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -17,7 +17,7 @@ A well-designed rendering pipeline is essential for creating a flexible and effi The following diagram provides a high-level overview of a modern Vulkan rendering pipeline: -image::../../../images/rendering_pipeline_flowchart.svg[Rendering Pipeline Flowchart, width=600, alt="Flowchart showing the stages of a modern Vulkan rendering pipeline"] +image::../../../images/rendering_pipeline_flowchart.png[Rendering Pipeline Flowchart, width=600, alt="Flowchart showing the stages of a modern Vulkan rendering pipeline"] [NOTE] ==== diff --git a/images/component_based_architecture_diagram.png b/images/component_based_architecture_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f089e2b83090638466b382b4df53f5dc8d3046 GIT binary patch literal 27752 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 literal 0 HcmV?d00001 diff --git a/images/component_based_architecture_diagram.svg b/images/component_based_architecture_diagram.svg deleted file mode 100644 index b4adb518..00000000 --- a/images/component_based_architecture_diagram.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - Component-Based Architecture - - - - Entity 1 - - - - Transform - - - Mesh - - - Physics - - - - Entity 2 - - - - Transform - - - Audio - - - AI - - - - Entity 3 - - - - Transform - - - Mesh - - - Light - - - - Render System - - - Physics System - - - Audio System - - - - - - - - - - - Systems process entities with specific components - Components define entity behavior and data - Entities are just containers for components - - - - - - - - - - - - - - - - - Legend - - - - Entity - - - - Component - - - - System - diff --git a/images/rendering_pipeline_flowchart.png b/images/rendering_pipeline_flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..8a7614e2903213aee1802a27bf57d287ba9bac90 GIT binary patch literal 38975 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

XOL$)4m5(Xu+S5D&NtuY?yY;g^yaJl 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

&m@ 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Ȃp1yO%>ziAK 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

I{*T&oMy-UOu?@XTL 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 z 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�UB8A`W|p-jB{fNAJqjKN*%5MNbw&L)k+ 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

V$%w&r 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

KPS+y@^E^!#Ibf7{ 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 U6syxW82|tP07*qoM6N<$f@JHT!vFvP literal 0 HcmV?d00001 From a58b884749ce427729950c9865871f54edf00299 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:17:48 -0700 Subject: [PATCH 056/102] committing requested changes in stages. --- .../Loading_Models/01_introduction.adoc | 39 +++--- .../Loading_Models/04_loading_gltf.adoc | 61 +++++++-- .../Loading_Models/05_pbr_rendering.adoc | 14 +- .../Loading_Models/06_multiple_objects.adoc | 17 +-- .../Loading_Models/07_scene_rendering.adoc | 125 ++++++++++++++---- .../Loading_Models/08_animations.adoc | 58 ++++++-- .../Loading_Models/09_conclusion.adoc | 9 +- .../Loading_Models/index.adoc | 9 +- 8 files changed, 229 insertions(+), 103 deletions(-) diff --git a/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc index 2906f138..876a927c 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Introduction @@ -15,26 +8,26 @@ Welcome to the "Loading Models" chapter of our "Building a Simple Engine" series In this chapter, we'll set up a robust model loading system that can handle modern 3D assets. Building upon the engine architecture we've established and the camera system we've implemented, we'll now add the ability to load and render complex 3D models. In the link:../../15_GLTF_KTX2_Migration.html[chapter on glTF and KTX2] from the main tutorial, we learned about migrating from OBJ to glTF format and the basics of loading glTF models. Now, we'll integrate that knowledge into our engine structure to create a more complete implementation. -In this chapter, we'll focus on: +This chapter will transform your understanding of 3D asset handling from simple model loading to sophisticated engine-level systems. We'll begin by building a scene graph, which provides the hierarchical organization that complex 3D scenes require. This foundation enables you to group objects logically, apply transformations at different levels, and efficiently manage scene complexity. -* Building a scene graph to organize 3D objects hierarchically -* Implementing animation support for glTF models -* Creating a PBR material system -* Rendering multiple objects with different transformations -* Structuring our code in a more engine-like way +Animation support forms a crucial part of modern 3D applications. We'll implement a system that can handle glTF's skeletal animations, giving life to your 3D models through smooth character movement, object animations, and complex multi-part systems. -This approach will serve as the foundation for our engine and allow us to create more complex scenes with animated models. +The PBR material system we'll create bridges the gap between the lighting concepts from previous chapters and real-world asset integration. You'll see how to map glTF material properties to your shaders seamlessly, creating a workflow that artists can understand and use effectively. + +Rendering multiple objects with different transformations presents both technical and organizational challenges. We'll solve these through careful engine architecture that can batch similar objects efficiently while maintaining the flexibility to handle unique materials and transformations per object. + +Throughout this implementation, we'll structure our code with engine-level thinking rather than tutorial-style solutions. This approach will serve you well as your projects grow in complexity and scope, providing a solid foundation for creating complex scenes with animated models. == Prerequisites -Before starting this chapter, you should have completed the main Vulkan tutorial up to at least Chapter 16 (Multiple Objects). You should also be familiar with: +This chapter builds on the foundation established in the main Vulkan tutorial, particularly Chapter 16 (Multiple Objects), as we'll extend those concepts to handle more complex scene organization and asset management. The multiple objects chapter introduced the basic concepts of rendering different geometry, which we'll now scale up to handle complete 3D models with materials and animations. + +You'll need solid familiarity with core Vulkan concepts that form the backbone of our model loading system. link:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] become more complex when handling multiple models with different materials, as we'll need to manage descriptor sets and push constants efficiently. Understanding link:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[graphics pipelines] is crucial since different materials might require different pipeline configurations. + +Experience with link:../../04_Vertex_buffers/00_Vertex_input_description.adoc[vertex] and link:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] translates directly to model loading, where glTF files contain vertex data in specific formats that we'll need to parse and upload to GPU buffers. link:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] knowledge becomes essential as we'll use them for transformation matrices, lighting information, and material properties. + +link:../../06_Texture_mapping/00_Images.adoc[Texture mapping] skills are particularly important since glTF models often include multiple textures per material (diffuse, normal, metallic-roughness, etc.), and we'll need to load and bind these textures efficiently. -* Basic Vulkan concepts: -** link:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] -** link:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] -* link:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and link:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] -* link:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] -* link:../../06_Texture_mapping/00_Images.adoc[Texture mapping] -* Basic 3D math (matrices, vectors, quaternions) - See link:../../Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc[Camera Transformations] for a refresher on 3D math. +Finally, basic 3D math understanding (matrices, vectors, quaternions) is crucial for handling model transformations, animations, and scene hierarchies. If you need a refresher, see the link:../../Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc[Camera Transformations chapter] for detailed coverage of these mathematical concepts. link:../GUI/06_conclusion.adoc[Previous: GUI] | link:02_project_setup.adoc[Next: Setting Up the Project] diff --git a/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc index def51751..aa07e210 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Understanding glTF -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Understanding glTF @@ -196,7 +189,11 @@ For our engine, we use the .glb format with embedded KTX2 textures. This approac The glTF specification supports embedded textures through the `bufferView` property of image objects. When using KTX2 textures, the `mimeType` is set to `"image/ktx2"` to indicate the format. -Here's how we extract material data and load embedded KTX2 textures from a glTF file: +The texture loading process involves several complex steps that bridge the gap between glTF's abstract texture references and Vulkan's low-level GPU resources. + +=== Texture Loading: glTF Texture Iteration and Metadata Extraction + +First, we iterate through the glTF model's texture definitions and extracting the fundamental information needed to locate and identify each texture resource. [source,cpp] ---- @@ -208,7 +205,18 @@ for (size_t i = 0; i < gltfModel.textures.size(); i++) { Texture tex; tex.name = image.name.empty() ? "texture_" + std::to_string(i) : image.name; +---- + +The glTF texture system uses an indirection approach where textures reference images, and images contain the actual pixel data or references to it. This separation allows multiple textures to share the same image data but with different sampling parameters (like different filtering or wrapping modes). Our iteration process builds a comprehensive inventory of all texture resources that materials will eventually reference. +The naming strategy provides essential debugging and asset management capabilities. When artists create textures in their 3D applications, meaningful names help developers identify which textures serve which purposes during development. The fallback naming scheme ensures every texture has a unique identifier even when artists haven't provided descriptive names. + +=== Texture Loading: Format Detection and Buffer Access + +Next, we need to figure out whether textures are embedded in the glTF file and identify their format, setting up the foundation for appropriate loading strategies. + +[source,cpp] +---- // Check if the image is embedded as KTX2 if (image.mimeType == "image/ktx2" && image.bufferView >= 0) { // Get the buffer view that contains the KTX2 data @@ -218,7 +226,18 @@ for (size_t i = 0; i < gltfModel.textures.size(); i++) { // Extract the KTX2 data from the buffer const uint8_t* ktx2Data = buffer.data.data() + bufferView.byteOffset; size_t ktx2Size = bufferView.byteLength; +---- + +The MIME type detection ensures we're working with KTX2 format specifically, which provides several advantages over traditional image formats like PNG or JPEG. KTX2 is designed specifically for GPU textures and supports advanced features like basis universal compression, multiple mipmap levels, and direct GPU format compatibility. The bufferView check confirms that the image data is embedded within the glTF file rather than referenced externally. + +The buffer access pattern demonstrates glTF's sophisticated data organization system. Rather than copying data unnecessarily, we obtain direct pointers to the KTX2 data within the loaded glTF buffer. This approach minimizes memory usage and avoids expensive copy operations, which is particularly important when dealing with large texture datasets that can easily consume hundreds of megabytes. +=== Texture Loading: KTX2 Parsing and Validation + +Now we need to load the KTX2 texture data using the specialized KTX-Software library and perform initial validation to ensure the texture data is usable. + +[source,cpp] +---- // Load the KTX2 texture using KTX-Software library ktxTexture2* ktxTexture = nullptr; KTX_error_code result = ktxTexture2_CreateFromMemory( @@ -231,7 +250,18 @@ for (size_t i = 0; i < gltfModel.textures.size(); i++) { std::cerr << "Failed to load KTX2 texture: " << ktxErrorString(result) << std::endl; continue; } +---- + +The KTX-Software library provides robust parsing of the complex KTX2 format, handling details like multiple mipmap levels, various pixel formats, and metadata that would be extremely complex to implement correctly from scratch. The `KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT` flag instructs the library to immediately load the actual pixel data into memory, preparing it for subsequent processing steps. + +Error handling at this stage is crucial because texture files can become corrupted during asset pipeline processing or file transfer. By continuing with the next texture when one fails to load, we ensure that a single problematic texture doesn't prevent the entire model from loading. This graceful degradation approach is essential for robust production systems where content issues shouldn't crash the application. + +=== Texture Loading: Basis Universal Transcoding +Next, we handle the transcoding process that converts Basis Universal compressed textures into GPU-native formats for optimal runtime performance. + +[source,cpp] +---- // If the texture uses Basis Universal compression, transcode it to a GPU-friendly format if (ktxTexture->isCompressed && ktxTexture2_NeedsTranscoding(ktxTexture)) { // Choose the appropriate format based on GPU capabilities @@ -253,7 +283,20 @@ for (size_t i = 0; i < gltfModel.textures.size(); i++) { continue; } } +---- +Basis Universal represents a revolutionary approach to texture compression that solves a fundamental problem in cross-platform development: different GPUs support different texture compression formats. Traditional approaches required storing multiple texture versions for different platforms, dramatically increasing storage requirements. Basis Universal stores textures in an intermediate format that can be quickly transcoded to any GPU-native format at load time. + +The format selection logic (shown in commented form) demonstrates how production systems handle GPU capability differences. Desktop GPUs typically support BC7 compression which provides excellent quality, while mobile GPUs often use ASTC or ETC2 formats. The transcoding process happens at runtime based on the actual capabilities of the target GPU, ensuring optimal performance and quality on every platform. + +The transcoding operation itself is computationally intensive but happens only once during asset loading. The resulting GPU-native format provides significantly better performance during rendering compared to uncompressed textures, making the upfront transcoding cost worthwhile. Failed transcoding attempts trigger cleanup of partially processed resources, preventing memory leaks in error conditions. + +=== Texture Loading: Vulkan Resource Creation and GPU Upload + +Finally, create the Vulkan resources needed for GPU rendering and uploads the processed texture data to video memory. + +[source,cpp] +---- // Create Vulkan image, memory, and view vk::Format format = static_cast(ktxTexture2_GetVkFormat(ktxTexture)); vk::Extent3D extent{ diff --git a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc index 93183191..4150edb5 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Implementing PBR for glTF Models -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Applying PBR to glTF Models @@ -66,6 +59,11 @@ This uniform buffer includes: ==== Push Constants for Materials +[NOTE] +==== +We introduced push constants earlier in link:../Lighting_Materials/03_push_constants.adoc[push constants]; here we focus on how the same mechanism carries glTF metallic‑roughness material knobs efficiently per draw. +==== + We'll use link:https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#descriptorsets-pushconstant[push constants] to pass material properties to the shader: [source,cpp] diff --git a/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc index 7300ce5d..e57d7efb 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Managing Multiple Objects -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Managing Multiple Objects in a 3D Scene @@ -471,6 +464,14 @@ vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); With hardware instancing set up, we can modify our rendering loop to draw all instances in a single call: +The same five steps apply here; the difference is in step 4 where we bind the instance buffer and draw N instances: + +* Begin and describe attachments +* Begin rendering, bind pipeline, set viewport/scissor +* Update camera UBO (view/projection) +* Bind per‑mesh vertex + index buffers and a per‑mesh instance buffer, then draw instanced +* End rendering and present + [source,cpp] ---- void drawFrame() { diff --git a/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc index 3de386bc..b20cfb34 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Rendering the Scene -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Rendering the Scene @@ -41,54 +34,117 @@ void renderScene(const vk::raii::CommandBuffer& commandBuffer, Model& model, con The `renderNode` function is the heart of our scene rendering system. It recursively traverses the scene graph, calculating the global transformation matrix for each node and rendering any meshes it contains: +=== Node traversal and transform calculation + +The rendering process begins with systematic traversal of the scene graph, where each node's transformation is calculated by combining its local transformation with its parent's accumulated transformation matrix. + [source,cpp] ---- void renderNode(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { for (const auto node : nodes) { - // Calculate global matrix for this node + // Calculate the cumulative transformation from root to current node + // This combines the parent's world transformation with this node's local transformation glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); +---- + +The transformation calculation represents the core of hierarchical scene graph rendering. Each node's `getLocalMatrix()` returns its transformation relative to its parent, which we then combine with the accumulated parent transformation using matrix multiplication. This mathematical operation effectively "chains" transformations down the hierarchy, ensuring that moving a parent node automatically moves all its children in world space. - // If this node has a mesh, render it +The order of multiplication is critical here: `parentMatrix * nodeLocalMatrix` ensures that the node's local transformation occurs first (in the node's local coordinate space), followed by the parent's transformation that places it in world space. This ordering preserves the hierarchical relationship where children are positioned relative to their parents. + +=== Mesh validation and rendering preparation + +Before rendering, we must validate that the node contains valid mesh data and has been properly uploaded to GPU buffers, ensuring robust rendering that handles incomplete or invalid scene graph nodes. + +[source,cpp] +---- + // Validate that this node has complete, renderable mesh data + // All conditions must be met for safe GPU rendering if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { +---- + +This validation step prevents rendering errors that could occur from incomplete scene graph nodes. Not every node in a scene graph necessarily contains renderable geometry - some nodes exist purely for organization or as transformation anchors for child objects. By checking for non-empty vertex and index arrays plus valid buffer indices, we ensure that we only attempt to render nodes that have been properly prepared with GPU resources. + +The buffer index checks (>= 0) are particularly important because they confirm that the mesh data has been successfully uploaded to GPU buffers and assigned valid indices in our buffer management system. Negative indices typically indicate uninitialized or failed buffer allocations. + +=== Material property configuration + +This material setup step translates high-level material descriptions into GPU-ready push constants that control the appearance and lighting properties of the rendered geometry. - // Set up push constants for material properties +[source,cpp] +---- + // Initialize push constants structure for material data transfer PushConstantBlock pushConstants{}; + // Configure material properties if a valid material is assigned if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { const auto& material = model.materials[node->mesh.materialIndex]; - pushConstants.baseColorFactor = material.baseColorFactor; - pushConstants.metallicFactor = material.metallicFactor; - pushConstants.roughnessFactor = material.roughnessFactor; + + // Set PBR material factors that control surface appearance + pushConstants.baseColorFactor = material.baseColorFactor; // Surface color tint + pushConstants.metallicFactor = material.metallicFactor; // Metallic vs. dielectric + pushConstants.roughnessFactor = material.roughnessFactor; // Surface roughness + + // Configure texture binding indices (-1 indicates no texture) pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; } else { - // Default material properties - pushConstants.baseColorFactor = glm::vec4(1.0f); - pushConstants.metallicFactor = 1.0f; - pushConstants.roughnessFactor = 1.0f; - pushConstants.baseColorTextureSet = 1; - pushConstants.physicalDescriptorTextureSet = -1; - pushConstants.normalTextureSet = -1; - pushConstants.occlusionTextureSet = -1; - pushConstants.emissiveTextureSet = -1; + // Apply sensible default material properties for unassigned materials + pushConstants.baseColorFactor = glm::vec4(1.0f); // White base color + pushConstants.metallicFactor = 1.0f; // Fully metallic (safe default) + pushConstants.roughnessFactor = 1.0f; // Fully rough (safe default) + pushConstants.baseColorTextureSet = 1; // Assume default texture + pushConstants.physicalDescriptorTextureSet = -1; // No metallic/roughness texture + pushConstants.normalTextureSet = -1; // No normal map + pushConstants.occlusionTextureSet = -1; // No ambient occlusion + pushConstants.emissiveTextureSet = -1; // No emissive texture } +---- - // Update model matrix in push constants - commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); +The material configuration system bridges the gap between artist-authored materials and GPU shader parameters. Push constants provide the fastest path for updating per-object material data, as they bypass the GPU's memory hierarchy and are directly accessible to shader cores. This makes them ideal for material properties that change frequently between draw calls. - // Bind vertex and index buffers +The texture index mapping system (-1 for unused, positive integers for active bindings) allows shaders to conditionally sample textures based on availability. This approach provides flexibility where some materials might have normal maps while others don't, without requiring different shader variants or complex branching logic. + +The default material properties are chosen conservatively to prevent rendering artifacts when materials are missing or improperly configured. Metallic and roughness values of 1.0 tend to produce visually acceptable results across different lighting conditions, though they may not represent the intended material appearance. + +=== GPU resource binding and draw command execution + +The final rendering phase binds GPU resources and issues the actual draw command that transforms the scene graph node into rendered pixels on the screen. + +[source,cpp] +---- + // Upload material properties to GPU via push constants + // This provides fast, per-draw-call material parameter updates + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, + 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind geometry data buffers for GPU access + // Vertex buffer contains position, normal, texture coordinate data commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + // Index buffer defines triangle connectivity and enables vertex reuse commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); - // Draw the mesh + // Execute the draw command to render this mesh + // GPU processes indices to generate triangles and runs vertex/fragment shaders commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); } +---- - // Recursively render children +The resource binding sequence follows Vulkan's explicit binding model where each resource type must be bound before use. Vertex buffers provide the per-vertex attribute data (positions, normals, texture coordinates), while index buffers define how vertices connect to form triangles. This indexed rendering approach reduces memory usage by allowing vertex reuse across multiple triangles. + +The `drawIndexed` command triggers GPU execution of the entire graphics pipeline for this mesh. The GPU processes each index to fetch vertex data, runs the vertex shader to transform geometry, rasterizes triangles to generate fragments, and executes the fragment shader to determine final pixel colors. All the material properties we configured via push constants become available to the fragment shader during this process. + +=== Hierarchical recursion + +Finally, ensure complete scene graph traversal by recursively processing child nodes with the accumulated transformation matrix, maintaining the hierarchical structure throughout the rendering process. + +[source,cpp] +---- + // Recursively process child nodes with accumulated transformation + // This maintains the hierarchical transformation chain down the scene graph if (!node->children.empty()) { renderNode(commandBuffer, node->children, nodeMatrix); } @@ -132,6 +188,11 @@ glm::mat4 getLocalMatrix() { ==== Material Setup +[NOTE] +==== +We covered PBR material theory and shader details earlier in Loading_Models/05_pbr_rendering.adoc, so we won’t restate that here. This section focuses on the wiring: how material properties are packed into push constants and consumed by the draw call in this chapter’s context. +==== + If the node has a mesh, we need to set up its material properties before rendering: [source,cpp] @@ -214,7 +275,13 @@ This ensures that all nodes in the scene graph are visited and rendered with the === Integrating Scene Rendering in the Main Loop -To use our scene rendering system in the main rendering loop, we need to set up the necessary Vulkan state and call the `renderScene` function: +To use our scene rendering system in the main rendering loop, we need to set up the necessary Vulkan state and call the `renderScene` function. To keep this digestible, think of the frame as five steps: + +1) Begin and describe attachments (dynamic rendering inputs) +2) Begin rendering, bind pipeline, set viewport/scissor +3) Update camera UBO (view/projection) +4) Traverse scene graph and issue per-mesh draws +5) End rendering and present [source,cpp] ---- diff --git a/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc b/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc index c8580a6b..a492e37f 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Updating Animations -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Understanding and Implementing Animations @@ -66,43 +59,88 @@ These structures work together to define how animations are stored and processed === How Animation Playback Works -The core of our animation system is the `updateAnimation` method in the Model class: +The animation update process is the heart of our animation system, responsible for translating time-based animation data into actual transformations applied to scene graph nodes. + +=== Animation Update: Validation and Time Management + +Before we start anything, we should validate that we have valid animation data and manage the progression of animation time, including looping behavior for cyclical animations. [source,cpp] ---- void Model::updateAnimation(uint32_t index, float deltaTime) { + // Validate animation data and index bounds if (animations.empty() || index >= animations.size()) { return; } + // Update animation timing with automatic looping Animation& animation = animations[index]; animation.currentTime += deltaTime; if (animation.currentTime > animation.end) { animation.currentTime = animation.start; } +---- + +Animation validation is critical for robust systems because not all models contain animations, and external code might request non-existent animation indices. By performing this check early, we avoid crashes and undefined behavior when working with static models or invalid animation requests. This defensive programming approach is essential in production game engines where content from various sources might have inconsistent animation data. + +Time management forms the foundation of animation playback, where the deltaTime parameter represents the elapsed time since the last update. This frame-rate independent approach ensures animations play at consistent speeds regardless of rendering performance. The automatic looping mechanism seamlessly restarts animations when they reach their end time, creating continuous motion that's essential for idle animations, walking cycles, and other repetitive movements. + +=== Animation Update: Channel Iteration and Sampler Access +New we iterate through all animation channels, establishing the connection between abstract animation data and the specific nodes in our scene graph that will receive transformation updates. + +[source,cpp] +---- + // Process each animation channel to update corresponding scene nodes for (auto& channel : animation.channels) { AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; +---- + +The channel iteration represents the heart of our animation-to-scene-graph mapping system. Each channel defines a specific transformation type (position, rotation, or scale) for a particular node in the scene hierarchy. This one-to-many relationship allows complex animations where multiple properties of multiple nodes can be animated simultaneously, enabling sophisticated character animations with dozens of moving parts. + +The sampler access pattern demonstrates the separation of concerns in our animation architecture. Samplers contain the actual keyframe data and interpolation logic, while channels define what gets animated. This design allows multiple channels to share the same sampler data, reducing memory usage when the same animation curve applies to different nodes or when different transformation components follow identical patterns. - // Find the current key frame +=== Animation Update: Keyframe Location and Interpolation Factor Calculation + +Next, locate the appropriate keyframes that surround the current animation time and calculate the precise interpolation factor needed for smooth transitions between discrete animation samples. + +[source,cpp] +---- + // Find the current keyframe pair that brackets the animation time for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { if (animation.currentTime >= sampler.inputs[i] && animation.currentTime <= sampler.inputs[i + 1]) { + // Calculate normalized interpolation factor between keyframes float t = (animation.currentTime - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]); +---- + +The keyframe search algorithm performs a linear scan to find the pair of keyframes that bracket the current animation time. While this approach has O(n) complexity, it's practical for typical animation data where keyframes are relatively sparse. Production systems often optimize this with binary search or by caching the last keyframe index, but the linear approach provides clarity for educational purposes and adequate performance for most real-world animation sequences. +The interpolation factor calculation creates a normalized value between 0.0 and 1.0 that represents exactly where the current time falls between two keyframes. When t=0.0, we're at the first keyframe; when t=1.0, we're at the second keyframe; values in between create smooth transitions. This mathematical foundation enables all the interpolation techniques that follow, whether for linear position changes or complex quaternion rotations. + +=== Animation Update: Property-Specific Interpolation and Node Updates + +Finally, apply the appropriate mathematical interpolation technique based on the transformation type, updating the actual scene graph nodes with the computed animation values. + +[source,cpp] +---- + // Apply transformation based on the specific animation channel type switch (channel.path) { case AnimationChannel::TRANSLATION: { + // Linear interpolation for position changes glm::vec3 start = sampler.outputsVec3[i]; glm::vec3 end = sampler.outputsVec3[i + 1]; channel.node->translation = glm::mix(start, end, t); break; } case AnimationChannel::ROTATION: { + // Spherical linear interpolation for smooth rotation transitions glm::quat start = glm::quat(sampler.outputsVec4[i].w, sampler.outputsVec4[i].x, sampler.outputsVec4[i].y, sampler.outputsVec4[i].z); glm::quat end = glm::quat(sampler.outputsVec4[i + 1].w, sampler.outputsVec4[i + 1].x, sampler.outputsVec4[i + 1].y, sampler.outputsVec4[i + 1].z); channel.node->rotation = glm::slerp(start, end, t); break; } case AnimationChannel::SCALE: { + // Linear interpolation for scaling transformations glm::vec3 start = sampler.outputsVec3[i]; glm::vec3 end = sampler.outputsVec3[i + 1]; channel.node->scale = glm::mix(start, end, t); diff --git a/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc b/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc index d3fe3fd0..54e45925 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion diff --git a/en/Building_a_Simple_Engine/Loading_Models/index.adoc b/en/Building_a_Simple_Engine/Loading_Models/index.adoc index 9bc778ff..7e8bf09e 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/index.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/index.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Loading Models: Integrating a glTF loader with animation and PBR -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Chapter Overview From 4b4a24349a0f18bff2491f62949a153154072e6b Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:22:04 -0700 Subject: [PATCH 057/102] committing requested changes in stages. --- .../Mobile_Development/01_introduction.adoc | 27 ++++------- .../02_platform_considerations.adoc | 9 +--- .../03_performance_optimizations.adoc | 20 ++++---- .../04_rendering_approaches.adoc | 9 +--- .../05_vulkan_extensions.adoc | 47 ++++++++++++++----- .../Mobile_Development/06_conclusion.adoc | 9 +--- .../Mobile_Development/index.adoc | 10 +--- 7 files changed, 61 insertions(+), 70 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc index d9b4cb91..c2e8e1f3 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Introduction to Mobile Development @@ -17,23 +10,21 @@ Mobile development presents unique challenges and opportunities for Vulkan appli === What We'll Cover -In this chapter, we'll explore: +This chapter will guide you through the complex landscape of mobile Vulkan development, where desktop assumptions often don't apply. We'll start by examining the platform-specific requirements of Android and iOS, which present unique challenges in setup, lifecycle management, and input handling. Mobile applications face constraints that desktop applications rarely encounter—sudden interruptions, battery concerns, and varying hardware capabilities all require careful consideration in your engine design. -* *Platform Considerations for Android and iOS*: We'll discuss the specific requirements and constraints of developing Vulkan applications for Android and iOS, including platform-specific setup, lifecycle management, and input handling. +Performance optimization takes on critical importance in mobile environments where every watt of power consumption and every millisecond of frame time affects user experience. We'll explore essential techniques like power-of-two texture usage and efficient texture formats, along with mobile-specific optimizations that can mean the difference between smooth performance and user frustration. -* *Performance Optimizations for Mobile*: We'll explore techniques for optimizing your engine for mobile hardware, focusing on power-of-two textures, efficient texture formats, and other mobile-specific optimizations. +Understanding the fundamental architectural differences between mobile and desktop GPUs becomes essential for effective optimization. We'll compare Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR) approaches, helping you understand why techniques that work well on desktop might perform poorly on mobile, and how to design rendering strategies that leverage mobile GPU strengths. -* *Rendering Approaches*: We'll compare Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR), understanding their implications for mobile GPU architectures. - -* *Vulkan Extensions for Mobile*: We'll explore extensions like VK_KHR_dynamic_rendering_local_read, VK_KHR_dynamic_rendering, and VK_EXT_shader_tile_image that can significantly improve performance on mobile devices. +Finally, we'll explore the Vulkan extensions specifically designed for mobile platforms. Extensions like VK_KHR_dynamic_rendering_local_read, VK_KHR_dynamic_rendering, and VK_EXT_shader_tile_image unlock performance opportunities that can dramatically improve your application's efficiency on mobile hardware, transforming acceptable performance into exceptional user experiences. === Prerequisites -Before diving into this chapter, you should be familiar with: +This chapter represents the culmination of everything we've built throughout the previous chapters, as mobile development requires deep integration with all engine systems. You'll need solid mastery of Vulkan fundamentals and the engine architecture we've developed, since mobile optimization often requires fine-tuning at every level—from resource management and rendering pipelines to memory allocation and synchronization. + +Modern C++ expertise becomes particularly valuable in mobile development, where performance constraints demand efficient code and careful resource management. C++17 and C++20 features like constexpr, structured bindings, and concepts help create mobile-optimized code that performs well under strict power and thermal limitations. -* The basics of Vulkan and our engine architecture from previous chapters -* Modern C++ concepts, particularly those introduced in C++17 and C++20 -* Basic understanding of mobile development concepts +Understanding basic mobile development concepts will provide crucial context for the platform-specific decisions we'll make. Mobile applications operate under constraints that desktop applications rarely face—app lifecycle events, varying screen densities, touch input paradigms, and the need to preserve battery life all influence how we design and implement our Vulkan engine for mobile platforms. Let's begin by exploring the platform considerations for Android and iOS. diff --git a/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc index 284da385..467eb51b 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development: Platform Considerations -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Platform Considerations for Android and iOS diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 0903c4ff..a11b75ec 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development: Performance Optimizations -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Performance Optimizations for Mobile @@ -15,6 +8,17 @@ Mobile devices have significantly different hardware constraints compared to des === Texture Optimizations +To keep the workflow concrete and digestible, think of texture optimization in three steps: + +1) Step 1: Ensure power‑of‑two (POT) dimensions (resize if needed) +2) Step 2: Pick a hardware‑compressed format supported by the device (ASTC → ETC2 → BC → fallback) +3) Step 3: Upload/update the Vulkan image and clean up staging resources + +[NOTE] +==== +We focus on mobile‑specific decisions here. For general Vulkan image creation, staging uploads, and descriptor setup, refer back to earlier chapters in the engine series—link:../Engine_Architecture/04_resource_management.adoc[Resource Management], link:../Engine_Architecture/05_rendering_pipeline.adoc[Rendering Pipeline]—or the Vulkan Guide (https://docs.vulkan.org/guide/latest/). +==== + Textures are often the largest consumers of memory in a graphics application. Optimizing them is crucial for mobile performance. ==== Power-of-Two Textures diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc index d38d1ea8..71352adf 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development: Rendering Approaches -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Rendering Approaches for Mobile GPUs diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc index 17c334f7..15c4c8ac 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development: Vulkan Extensions -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Vulkan Extensions for Mobile @@ -25,9 +18,13 @@ The `VK_KHR_dynamic_rendering` extension (now part of Vulkan 1.3 core) allows yo 2. *Enables More Flexible Rendering*: Makes it easier to implement techniques that don't fit well into the traditional render pass model. 3. *Reduces API Overhead*: Fewer objects to create and manage means less CPU overhead. -==== Implementation +==== Implementation (Step-by-step) + +Let's break the setup into a few small, focused steps. + +===== Enable the extension and load entry points -Here's how to use dynamic rendering: +We first enable the device extension and, if you're not on Vulkan 1.3 core, load the function pointers. [source,cpp] ---- @@ -44,8 +41,16 @@ PFN_vkCmdBeginRenderingKHR vkCmdBeginRenderingKHR = PFN_vkCmdEndRenderingKHR vkCmdEndRenderingKHR = reinterpret_cast( vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR")); +---- + +This prepares your device to use dynamic rendering and gives access to the commands needed to begin/end a rendering scope without a traditional render pass. + +===== Describe attachments for this rendering scope -// Begin rendering +We define the color and depth attachments and package them into a VkRenderingInfoKHR. Think of this as an inline, one-off description of what would normally be baked into render pass/framebuffer objects. + +[source,cpp] +---- VkRenderingAttachmentInfoKHR color_attachment{}; color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR; color_attachment.imageView = color_image_view; @@ -69,7 +74,16 @@ rendering_info.layerCount = 1; rendering_info.colorAttachmentCount = 1; rendering_info.pColorAttachments = &color_attachment; rendering_info.pDepthAttachment = &depth_attachment; +---- + +Each frame (or subpass-equivalent), you can tweak these descriptors directly (e.g., swapchain views after resize), avoiding pipeline-wide re-creation. +===== Begin rendering, draw, end rendering + +With the attachments described, we open the rendering scope, record draws, then close the scope. + +[source,cpp] +---- vkCmdBeginRenderingKHR(command_buffer, &rendering_info); // Record drawing commands @@ -80,7 +94,11 @@ vkCmdDraw(command_buffer, vertex_count, 1, 0, 0); vkCmdEndRenderingKHR(command_buffer); ---- -When using C++ bindings: +The begin/end pair replaces vkCmdBeginRenderPass/vkCmdEndRenderPass while providing more flexibility for modern rendering flows. + +===== C++ bindings (vulkan.hpp) variant + +If you're using vulkan.hpp (vk::), the structure population is more ergonomic but follows the same steps. [source,cpp] ---- @@ -104,7 +122,12 @@ rendering_info.setRenderArea(render_area); rendering_info.setLayerCount(1); rendering_info.setColorAttachments(color_attachment); rendering_info.setPDepthAttachment(&depth_attachment); +---- + +Once the description is assembled, begin the rendering scope, submit draws, and end the scope. +[source,cpp] +---- command_buffer.beginRenderingKHR(rendering_info); // Record drawing commands diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc index 11b25ade..a002f05a 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion diff --git a/en/Building_a_Simple_Engine/Mobile_Development/index.adoc b/en/Building_a_Simple_Engine/Mobile_Development/index.adoc index e8096b8b..d4975d67 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/index.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/index.adoc @@ -1,13 +1,7 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Mobile Development -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ + This chapter covers the essential aspects of adapting your Vulkan engine for mobile platforms, focusing on Android and iOS development, performance optimizations, rendering approaches, and mobile-specific Vulkan extensions. From 6ec73219e6e295fefd96cfd6b8881a67b59d7535 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:31:41 -0700 Subject: [PATCH 058/102] committing requested changes in stages. --- .../Subsystems/01_introduction.adoc | 33 +++++++------------ .../Subsystems/03_vulkan_audio.adoc | 9 +---- .../Subsystems/04_physics_basics.adoc | 18 +++++----- .../Subsystems/05_vulkan_physics.adoc | 21 +++++++----- .../Subsystems/06_conclusion.adoc | 9 +---- .../Subsystems/index.adoc | 9 +---- 6 files changed, 37 insertions(+), 62 deletions(-) diff --git a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc index 5b6794a8..6b34cc9e 100644 --- a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Subsystems: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Introduction to Engine Subsystems @@ -17,25 +10,23 @@ These subsystems are essential for creating immersive and interactive experience === What We'll Cover -In this chapter, we'll explore: +This chapter will take you through implementing two crucial engine subsystems that bring games and simulations to life. We'll begin with an audio subsystem, starting from the fundamentals of playing sounds and music, then advancing to sophisticated techniques like Head-Related Transfer Function (HRTF) processing for convincing 3D spatial audio. The progression shows how Vulkan compute shaders can transform basic audio playback into immersive soundscapes that respond to your 3D world. -* *Audio Subsystem*: We'll implement a basic audio system capable of playing sounds and music, and then explore how Vulkan compute shaders can be used for advanced audio processing techniques like Head-Related Transfer Function (HRTF) for 3D spatial audio. - -* *Physics Subsystem*: We'll create a simple physics system for collision detection and response, and then demonstrate how Vulkan compute shaders can accelerate physics calculations for large numbers of objects. +Our physics subsystem follows a similar path, beginning with essential collision detection and response mechanisms that make objects interact believably. As we develop these foundations, we'll demonstrate how Vulkan's parallel processing capabilities can accelerate physics calculations dramatically, enabling simulations with large numbers of interacting objects that would overwhelm traditional CPU-based approaches. Throughout this chapter, we'll continue our modern C++ approach from previous chapters. === Why Vulkan for Audio and Physics? -You might be wondering why we'd use a graphics API like Vulkan for audio processing and physics simulations. There are several compelling reasons: +The decision to use Vulkan for audio processing and physics simulations might seem unconventional at first, but it represents a forward-thinking approach to engine development that leverages modern hardware capabilities. -* *Computational Power*: Modern GPUs offer massive parallel processing capabilities that can be harnessed for non-graphical tasks through compute shaders. +Modern GPUs provide massive parallel processing power through thousands of cores designed for simultaneous computation. Through Vulkan's compute shaders, we can harness this computational muscle for tasks far beyond graphics rendering. Audio processing benefits tremendously from parallel operations—imagine processing dozens of simultaneous sound sources with real-time spatial effects, or running complex physics simulations with thousands of interacting objects. -* *Unified Memory Model*: With Vulkan, we can share memory between graphics, audio, and physics processing, reducing data transfer overhead. +Vulkan's unified memory model creates opportunities for efficiency that traditional separated approaches cannot match. When graphics, audio, and physics processing share memory spaces, we eliminate the costly data transfers that would otherwise shuttle information between CPU and GPU repeatedly. This shared memory architecture enables sophisticated interactions—physics simulations can directly influence particle systems, audio processing can respond to visual effects, and all systems can work together seamlessly. -* *Cross-Platform Consistency*: By using Vulkan for these subsystems, we maintain a consistent implementation across different platforms. +Cross-platform consistency becomes increasingly valuable as projects target multiple devices. By implementing these subsystems through Vulkan, we maintain identical behavior across Windows, Linux, mobile platforms, and emerging devices. This consistency reduces debugging time and ensures that audio and physics behavior remains predictable regardless of deployment target. -* *Reduced CPU Load*: Offloading intensive calculations to the GPU frees up CPU resources for other game logic. +The performance benefits extend beyond raw computational power. Offloading intensive calculations to the GPU frees CPU resources for game logic, scripting, AI processing, and other tasks that require sequential processing or complex branching. This separation allows each processor type to focus on tasks it handles most efficiently. Additionally, the intention here is to offer a perspective of using Vulkan for more than just Graphics in your application. Our goal with this tutorial @@ -45,11 +36,11 @@ for more than just Graphics in your application. Our goal with this tutorial === Prerequisites -Before diving into this chapter, you should be familiar with: +This chapter builds extensively on the engine architecture and Vulkan foundations established in previous chapters. The modular design patterns we've implemented become crucial when adding subsystems that need to integrate cleanly with existing rendering, camera, and resource management systems. + +Experience with Vulkan compute shaders is essential, as we'll leverage compute capabilities to accelerate both audio processing and physics calculations. If you haven't worked through the compute shader sections in the main tutorial, review them before proceeding—the parallel processing concepts and GPU memory management techniques translate directly to our subsystem implementations. -* The basics of Vulkan and our engine architecture from previous chapters -* Compute shaders in Vulkan (covered in the main tutorial) -* Basic understanding of audio and physics concepts in game development +A basic understanding of audio and physics concepts in game development will help you appreciate the design decisions we make throughout the implementation. While we'll explain the fundamentals as we build each system, familiarity with concepts like sound attenuation, collision detection, and rigid body dynamics will deepen your understanding of how these subsystems serve the broader goals of interactive applications. Let's begin by exploring how to implement a basic audio subsystem and then enhance it with Vulkan's computational capabilities. diff --git a/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc index a8e828ad..d7eec7bf 100644 --- a/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Subsystems: Vulkan for Audio Processing -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Enhancing Audio with Vulkan diff --git a/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc b/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc index aa9bbf9c..5ade0ea9 100644 --- a/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Subsystems: Physics Basics -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Physics System Fundamentals @@ -274,6 +267,15 @@ void Engine::Shutdown() { === Basic Implementation of Physics Simulation +To keep the update loop easy to follow, think of a fixed‑timestep frame as six steps: + +1) Accumulate forces (e.g., gravity, user forces) +2) Integrate forces (update velocities with damping) +3) Detect collisions (broad/narrow checks per pair) +4) Resolve collisions (impulses + positional correction) +5) Integrate velocities (update positions and orientations) +6) Clear forces (prepare for next step) + Let's implement the core physics simulation functions: [source,cpp] diff --git a/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc b/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc index 945a832d..6157f080 100644 --- a/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Subsystems: Vulkan for Physics Simulation -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Enhancing Physics with Vulkan @@ -43,7 +36,17 @@ To implement GPU-accelerated physics, we'll need to: 2. Create compute shaders to perform physics calculations 3. Integrate the GPU physics with our existing CPU-based system -Let's extend our physics system to include Vulkan-accelerated components: +Let's extend our physics system to include Vulkan-accelerated components. We’ll approach it in four steps: + +1) Step 1: Data layout (GPUPhysicsData/GPUCollisionData structures) +2) Step 2: GPU resource setup (descriptor set layout, pipelines, storage buffers, descriptor sets) +3) Step 3: Simulation dispatch (integrate → broad‑phase → narrow‑phase → resolve with pipeline barriers) +4) Step 4: Synchronization and readback (update GPU buffers, submit, read back state, integrate in Update) + +[NOTE] +==== +We avoid repeating Vulkan compute fundamentals here; focus stays on physics‑specific wiring. Use earlier chapters (link:../Engine_Architecture/04_resource_management.adoc[Resource Management], link:../Engine_Architecture/05_rendering_pipeline.adoc[Rendering Pipeline]) or the Vulkan Guide (https://docs.vulkan.org/guide/latest/) if you need a refresher on descriptors, buffers, or pipeline creation. +==== [source,cpp] ---- diff --git a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc index 50540c5e..6e3fd900 100644 --- a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Subsystems: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion diff --git a/en/Building_a_Simple_Engine/Subsystems/index.adoc b/en/Building_a_Simple_Engine/Subsystems/index.adoc index 58f6923d..2b9c41e1 100644 --- a/en/Building_a_Simple_Engine/Subsystems/index.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/index.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Subsystems -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ This chapter covers the implementation of critical engine subsystems - Audio and Physics - with a focus on leveraging Vulkan's compute capabilities for enhanced performance. From 8ecae4e4acc130ce09d13008dfc7c7a531016351 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:33:52 -0700 Subject: [PATCH 059/102] committing requested changes in stages. --- .../install_dependencies_windows.bat | 186 ++++-------------- attachments/simple_engine/vcpkg.json | 12 ++ .../Tooling/01_introduction.adoc | 27 +-- .../Tooling/02_cicd.adoc | 9 +- .../Tooling/03_debugging_and_renderdoc.adoc | 9 +- .../Tooling/04_crash_minidump.adoc | 9 +- .../Tooling/05_extensions.adoc | 9 +- .../06_packaging_and_distribution.adoc | 9 +- .../Tooling/07_conclusion.adoc | 9 +- .../Tooling/index.adoc | 9 +- en/Building_a_Simple_Engine/introduction.adoc | 25 +-- 11 files changed, 65 insertions(+), 248 deletions(-) create mode 100644 attachments/simple_engine/vcpkg.json diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index 84bff6d7..2bcfc2e2 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -4,160 +4,40 @@ REM This script installs all required dependencies for building the Simple Game echo Installing Simple Game Engine dependencies for Windows... -REM Check if running as administrator -REM Administrator privileges are not required. Proceeding without elevation. -net session >nul 2>&1 -if %errorLevel% == 0 ( - echo Running as administrator (optional). -) else ( - echo Running without administrator privileges. -) - -REM vcpkg detection and optional local install -set "VCPKG_EXE=" -where vcpkg >nul 2>&1 -if %errorlevel%==0 ( - echo vcpkg found in PATH - set "VCPKG_EXE=vcpkg" -) else ( - echo vcpkg not found in PATH. - set "VCPKG_HOME=%USERPROFILE%\vcpkg" - set /p INSTALL_VCPKG="Install vcpkg locally to %VCPKG_HOME%? (Y/N): " - if /I "%INSTALL_VCPKG%"=="Y" ( - if not exist "%VCPKG_HOME%" ( - echo Cloning vcpkg into %VCPKG_HOME% ... - git clone https://github.com/Microsoft/vcpkg.git "%VCPKG_HOME%" - if %errorlevel% neq 0 ( - echo Failed to clone vcpkg. Ensure Git is installed and try again. - goto AFTER_VCPKG - ) - ) else ( - echo vcpkg directory already exists at %VCPKG_HOME% - ) - pushd "%VCPKG_HOME%" - call bootstrap-vcpkg.bat - if %errorlevel% neq 0 ( - echo Failed to bootstrap vcpkg. - popd - goto AFTER_VCPKG - ) - popd - set "VCPKG_EXE=%VCPKG_HOME%\vcpkg.exe" - set /p ADD_VCPKG_PATH="Add vcpkg to PATH for this session? (Y/N): " - if /I "%ADD_VCPKG_PATH%"=="Y" set "PATH=%PATH%;%VCPKG_HOME%" - ) else ( - echo Skipping vcpkg installation. - ) -) -:AFTER_VCPKG - -REM Tool checks (no forced install) -where cmake >nul 2>&1 -if %errorlevel%==0 ( - echo CMake found in PATH -) else ( - echo CMake not found in PATH. - set /p OPEN_CMAKE="Open CMake download page in browser? (Y/N): " - if /I "%OPEN_CMAKE%"=="Y" start "" "https://cmake.org/download/" -) - -where git >nul 2>&1 -if %errorlevel%==0 ( - echo Git found in PATH -) else ( - echo Git not found in PATH. - set /p OPEN_GIT="Open Git for Windows download page? (Y/N): " - if /I "%OPEN_GIT%"=="Y" start "" "https://git-scm.com/download/win" -) - -REM Vulkan SDK detection (no forced install) -set "HAVE_VULKAN_SDK=" -if defined VULKAN_SDK set "HAVE_VULKAN_SDK=1" -where vulkaninfo >nul 2>&1 -if %errorlevel%==0 set "HAVE_VULKAN_SDK=1" -if defined HAVE_VULKAN_SDK ( - echo Vulkan SDK detected. -) else ( - echo Vulkan SDK not detected. - set /p OPEN_VULKAN="Open Vulkan SDK download page (LunarG) in browser? (Y/N): " - if /I "%OPEN_VULKAN%"=="Y" start "" "https://vulkan.lunarg.com/sdk/home#windows" -) - -REM Optional vcpkg package installation -if defined VCPKG_EXE ( - set /p INSTALL_VCPKG_PKGS="Install common dependencies via vcpkg (glfw3, glm, openal-soft, ktx, tinygltf)? (Y/N): " - if /I "%INSTALL_VCPKG_PKGS%"=="Y" ( - set "VCPKG_DEFAULT_TRIPLET=x64-windows" - echo Installing packages with %VCPKG_EXE% (triplet %VCPKG_DEFAULT_TRIPLET%) ... - "%VCPKG_EXE%" install glfw3:%VCPKG_DEFAULT_TRIPLET% glm:%VCPKG_DEFAULT_TRIPLET% openal-soft:%VCPKG_DEFAULT_TRIPLET% ktx:%VCPKG_DEFAULT_TRIPLET% tinygltf:%VCPKG_DEFAULT_TRIPLET% - if %errorlevel% neq 0 ( - echo Warning: Some vcpkg installations may have failed. Please review output. - ) - ) else ( - echo Skipping vcpkg package installation. - ) -) else ( - echo vcpkg not available; skipping vcpkg package installation. -) - -REM Slang compiler detection and optional install -set "SLANGC_EXE=" -where slangc >nul 2>&1 -if %errorlevel%==0 ( - echo Slang compiler found in PATH - set "SLANGC_EXE=slangc" -) else ( - if defined VULKAN_SDK ( - if exist "%VULKAN_SDK%\Bin\slangc.exe" set "SLANGC_EXE=%VULKAN_SDK%\Bin\slangc.exe" - if not defined SLANGC_EXE if exist "%VULKAN_SDK%\Bin64\slangc.exe" set "SLANGC_EXE=%VULKAN_SDK%\Bin64\slangc.exe" - ) -) - -if defined SLANGC_EXE ( - echo Using Slang at %SLANGC_EXE% -) else ( - echo Slang compiler (slangc) not found. - set /p INSTALL_SLANG="Download and install latest Slang locally (no admin)? (Y/N): " - if /I "%INSTALL_SLANG%"=="Y" ( - set "SLANG_ROOT=%LOCALAPPDATA%\slang" - if not exist "%SLANG_ROOT%" mkdir "%SLANG_ROOT%" - echo Downloading latest Slang release... - powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $r=Invoke-RestMethod 'https://api.github.com/repos/shader-slang/slang/releases/latest'; $asset=$r.assets | Where-Object { $_.name -match 'win64.*\.zip$' } | Select-Object -First 1; if(-not $asset){ throw 'No win64 asset found'; } $out=Join-Path $env:TEMP $asset.name; Invoke-WebRequest $asset.browser_download_url -OutFile $out; Expand-Archive -Path $out -DestinationPath $env:LOCALAPPDATA\slang -Force; Write-Host ('Downloaded Slang ' + $r.tag_name)" - echo Locating slangc.exe... - set "SLANGC_PATH=" - for /f "delims=" %%F in ('dir /b /s "%LOCALAPPDATA%\slang\slangc.exe" 2^>nul') do ( - set "SLANGC_PATH=%%F" - goto FOUND_SLANG - ) - :FOUND_SLANG - if defined SLANGC_PATH ( - echo Found slangc at "%SLANGC_PATH%" - for %%D in ("%SLANGC_PATH%") do set "SLANG_BIN=%%~dpD" - set /p ADD_SLANG_PATH="Add Slang to PATH for this session? (Y/N): " - if /I "%ADD_SLANG_PATH%"=="Y" set "PATH=%SLANG_BIN%;%PATH%" - set "SLANGC_EXE=%SLANGC_PATH%" - ) else ( - echo Failed to locate slangc after extraction. Please install manually if needed: https://github.com/shader-slang/slang/releases - ) - ) else ( - echo Skipping Slang installation. - ) -) - -REM Final guidance (no machine-wide env changes) +:: Check if vcpkg is installed +where vcpkg >nul 2>nul +if %ERRORLEVEL% neq 0 ( + echo vcpkg not found. Please install vcpkg first. + echo Visit https://github.com/microsoft/vcpkg for installation instructions. + echo Typically, you would: + echo 1. git clone https://github.com/Microsoft/vcpkg.git + echo 2. cd vcpkg + echo 3. .\bootstrap-vcpkg.bat + echo 4. Add vcpkg to your PATH + exit /b 1 +) + +:: Enable binary caching for vcpkg +echo Enabling binary caching for vcpkg... +set VCPKG_BINARY_SOURCES=clear;files,%TEMP%\vcpkg-cache,readwrite + +:: Create cache directory if it doesn't exist +if not exist %TEMP%\vcpkg-cache mkdir %TEMP%\vcpkg-cache + +:: Install all dependencies at once using vcpkg with parallel installation +echo Installing all dependencies... +vcpkg install --triplet=x64-windows --x-manifest-root=%~dp0\.. --feature-flags=binarycaching,manifests --x-install-root=%VCPKG_INSTALLATION_ROOT%/installed + +:: Remind about Vulkan SDK echo. -echo Dependencies check completed! +echo Don't forget to install the Vulkan SDK from https://vulkan.lunarg.com/ echo. -echo Build instructions: -echo 1. Open a new command prompt (if you added tools to PATH for this session). -echo 2. cd to attachments\simple_engine -echo 3. mkdir build ^&^& cd build -echo 4. If using vcpkg toolchain, run: -echo cmake .. -DCMAKE_TOOLCHAIN_FILE=%VCPKG_HOME%\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -echo (adjust path if you installed vcpkg elsewhere; or omit this flag if not using vcpkg) -echo 5. cmake --build . --config Release -echo. -echo Alternatively open CMakeLists.txt in Visual Studio and configure normally. + +echo All dependencies have been installed successfully! +echo You can now use CMake to build your Vulkan project. echo. +echo Example CMake command: +echo cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path\to\vcpkg]\scripts\buildsystems\vcpkg.cmake +echo cmake --build build -pause +exit /b 0 diff --git a/attachments/simple_engine/vcpkg.json b/attachments/simple_engine/vcpkg.json new file mode 100644 index 00000000..d246de0d --- /dev/null +++ b/attachments/simple_engine/vcpkg.json @@ -0,0 +1,12 @@ +{ + "name": "vulkan-game-engine-tutorial", + "version": "1.0.0", + "dependencies": [ + "glfw3", + "glm", + "openal-soft", + "ktx", + "tinygltf", + "nlohmann-json" + ] +} diff --git a/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc index be03736e..7b2ddfb1 100644 --- a/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Introduction to Engine Tooling @@ -17,23 +10,21 @@ Effective tooling is critical for maintaining productivity, ensuring quality, an === What We'll Cover -In this chapter, we'll explore: +This chapter will equip you with the professional tooling ecosystem that transforms a working Vulkan application into a maintainable, debuggable, and deployable product. We'll begin by implementing a continuous integration and continuous deployment pipeline specifically designed for Vulkan's unique requirements. This foundation ensures that your application builds consistently across platforms while catching integration issues before they reach users. -* *CI/CD for Vulkan Projects*: We'll implement a continuous integration and continuous deployment pipeline specifically tailored for Vulkan applications, ensuring consistent builds and automated testing across different platforms. +Debugging Vulkan applications presents unique challenges that traditional debugging approaches can't address effectively. We'll master both Vulkan's built-in debugging extensions like VK_KHR_debug_utils and external tools like RenderDoc, creating a comprehensive debugging workflow that can diagnose everything from validation layer warnings to complex rendering pipeline issues. -* *Debugging with VK_KHR_debug_utils and RenderDoc*: We'll explore how to use Vulkan's debugging extensions and external tools like RenderDoc to identify and fix issues in your rendering pipeline. +Robust crash handling becomes crucial as your application moves toward production deployment. We'll implement systems that can gracefully handle unexpected failures, generate detailed minidumps for post-mortem analysis, and provide users with meaningful recovery options rather than abrupt terminations. -* *Crash Handling and Minidumps*: We'll implement robust crash handling mechanisms and learn how to generate and analyze minidumps to diagnose issues in production environments. - -* *Vulkan Extensions for Robustness*: We'll explore extensions like VK_EXT_robustness2 that can help make your application more resilient to undefined behavior. +Finally, we'll explore Vulkan extensions designed specifically for application robustness, such as VK_EXT_robustness2, which help your application handle edge cases and undefined behavior gracefully. These extensions transform potential crashes into recoverable situations, improving the overall user experience. === Prerequisites -Before diving into this chapter, you should be familiar with: +This chapter assumes solid understanding of the Vulkan fundamentals and engine architecture we've built throughout the previous chapters. The tooling we'll implement needs to integrate with your existing systems—CI/CD pipelines must understand your project structure, debugging tools must work with your rendering pipeline, and crash handling must respect your engine's resource management patterns. + +Experience with modern C++ concepts becomes particularly important here, as professional tooling often leverages advanced language features for reliability and maintainability. C++17 and C++20 features like structured bindings, concepts, and coroutines appear frequently in production tooling code, and understanding these patterns will help you implement robust solutions. -* The basics of Vulkan and our engine architecture from previous chapters -* Modern C++ concepts, particularly those introduced in C++17 and C++20 -* Basic understanding of software development workflows and tools +A basic familiarity with software development workflows and tools will provide context for the systems we'll build. While we'll explain the specific implementations, understanding why continuous integration matters, how debugging fits into development cycles, and why crash reporting improves user experience will help you appreciate the architectural decisions we make throughout this chapter. Let's begin by exploring how to set up a CI/CD pipeline for Vulkan projects. diff --git a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc index a3e975cc..45f0625a 100644 --- a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc +++ b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: CI/CD for Vulkan Projects -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Continuous Integration and Deployment for Vulkan diff --git a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc index 93564766..f2324638 100644 --- a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc +++ b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: Debugging with VK_KHR_debug_utils and RenderDoc -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Debugging Vulkan Applications diff --git a/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc index 3c8a476c..165159a2 100644 --- a/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc +++ b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: Crash Handling and Minidumps -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Crash Handling in Vulkan Applications diff --git a/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc index 0ec71b3c..b7013eb7 100644 --- a/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc +++ b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: Vulkan Extensions for Robustness -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Vulkan Extensions for Robustness diff --git a/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc b/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc index 426fd0b8..dbc8dff3 100644 --- a/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc +++ b/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: Packaging and Distribution -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Packaging and Distributing Vulkan Applications diff --git a/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc index 75013f99..f5c4451b 100644 --- a/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion diff --git a/en/Building_a_Simple_Engine/Tooling/index.adoc b/en/Building_a_Simple_Engine/Tooling/index.adoc index d781357a..511903ab 100644 --- a/en/Building_a_Simple_Engine/Tooling/index.adoc +++ b/en/Building_a_Simple_Engine/Tooling/index.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Tooling -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ This chapter covers essential tooling and techniques for developing, debugging, and distributing Vulkan applications, with a focus on using modern C++20 modules and the vk::raii namespace. diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc index bb187795..9909d854 100644 --- a/en/Building_a_Simple_Engine/introduction.adoc +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Building a Simple Engine: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c{pp} == Introduction @@ -17,14 +10,7 @@ Welcome to the "Building a Simple Engine" tutorial series! This series marks a t While the previous tutorial series focused on introducing individual Vulkan concepts step by step, this series takes a different approach: -* *Intermediate to Advanced Level* - This tutorial assumes you have completed - the original link:../00_Introduction.adoc[Vulkan Tutorial] series and are - comfortable with the - fundamental concepts. If this feels too advanced, start with the link:../00_Introduction.adoc[Vulkan Tutorial] and come back when you're ready. - -* *Concept-Focused Learning* - Rather than providing exhaustive details on every possible implementation approach, we focus on key architectural concepts and design patterns for building a rendering engine. - -* *More Independent Work* - This series will require more time and independent problem-solving from you. We'll provide the framework and guidance, but you'll need to fill in some details on your own. Remember that you have access to many resources: the https://docs.vulkan.org/guide/latest/[Vulkan Guide] provides in-depth information on topics this tutorial only introduces, the https://docs.vulkan.org/samples/latest/[Samples] offer hands-on demonstrations of specific implementations, and the https://docs.vulkan.org/spec/latest/[Specification] contains the precise rules of the API. If you need assistance, don't hesitate to reach out—Vulkan has a vibrant community with experts across a wide range of topics. +This series targets readers who have completed the link:../00_Introduction.adoc[Vulkan Tutorial] and feel comfortable with the fundamentals. We’ll emphasize architectural concepts and design patterns over exhaustive API permutations, so you develop an engine mindset rather than a collection of snippets. Expect to do more independent work: fill in smaller gaps, experiment, and lean on the https://docs.vulkan.org/guide/latest/[Vulkan Guide], https://docs.vulkan.org/samples/latest/[Samples], and https://docs.vulkan.org/spec/latest/[Specification] as primary references. If a topic feels too advanced, revisit the original tutorial and return when ready. === What to Expect @@ -42,14 +28,11 @@ The "Building a Simple Engine" series is designed as a starting point for your j Throughout this series, we encourage you to experiment, extend the provided examples, and even challenge some of our design decisions. The best way to learn engine development is by doing, and sometimes by making (and learning from) mistakes. -=== How to Use This Tutorial +Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. -Each chapter in this series builds upon the previous ones, gradually constructing a simple but functional rendering engine. We recommend: +=== How to Use This Tutorial -1. Read through each chapter completely before implementing the code. -2. Take time to understand the concepts before moving on. -3. Don't hesitate to revisit the original Vulkan tutorial if you need to refresh your understanding of specific Vulkan concepts. -4. Experiment with the code and try to extend it with your own features. +Each chapter builds on the last to assemble a small but capable engine. Read a chapter end‑to‑end first, then implement; pause to internalize the concepts; and don’t hesitate to revisit the original Vulkan tutorial when you need a refresher. Treat the code as a starting point—experiment and extend it with your own features. Let's begin our journey into engine development with these chapters: From 345b137cab79233c5d318d98225949e2e4561de8 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:37:43 -0700 Subject: [PATCH 060/102] committing requested changes in stages. --- .../simple_engine/fetch_bistro_assets.bat | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/attachments/simple_engine/fetch_bistro_assets.bat b/attachments/simple_engine/fetch_bistro_assets.bat index 57efdcd6..fc4941bb 100644 --- a/attachments/simple_engine/fetch_bistro_assets.bat +++ b/attachments/simple_engine/fetch_bistro_assets.bat @@ -2,7 +2,7 @@ setlocal enabledelayedexpansion REM Fetch the Bistro example assets into the desired assets directory. -REM Default target: assets\bistro at the repository root. +REM Default target when run from attachments\simple_engine: Assets\bistro REM Usage: REM fetch_bistro_assets.bat [target-dir] REM Example: @@ -12,43 +12,50 @@ set REPO_SSH=git@github.com:gpx1000/bistro.git set REPO_HTTPS=https://github.com/gpx1000/bistro.git if "%~1"=="" ( - set TARGET_DIR=Assets\bistro + set "TARGET_DIR=Assets\bistro" ) else ( - set TARGET_DIR=%~1 + set "TARGET_DIR=%~1" ) -REM Ensure parent directory exists -for %%I in ("%TARGET_DIR%") do set PARENT=%%~dpI -if not exist "%PARENT%" mkdir "%PARENT%" +REM Ensure parent directory exists (avoid trailing backslash quoting issue by appending a dot) +for %%I in ("%TARGET_DIR%") do set "PARENT=%%~dpI" +if not exist "%PARENT%." mkdir "%PARENT%." REM If directory exists and is a git repo, update it; otherwise clone it if exist "%TARGET_DIR%\.git" ( echo Updating existing bistro assets in %TARGET_DIR% - pushd "%TARGET_DIR%" + pushd "%TARGET_DIR%" >nul 2>nul git pull --ff-only - popd + if errorlevel 1 ( + echo ERROR: Failed to update repository at %TARGET_DIR%. + popd >nul 2>nul + endlocal & exit /b 1 + ) + popd >nul 2>nul ) else ( echo Cloning bistro assets into %TARGET_DIR% REM Try SSH first; fall back to HTTPS on failure - git clone --depth 1 "%REPO_SSH%" "%TARGET_DIR%" 2>nul - if %ERRORLEVEL% neq 0 ( + git clone --depth 1 "%REPO_SSH%" "%TARGET_DIR%" 1>nul 2>nul + if errorlevel 1 ( echo SSH clone failed, trying HTTPS git clone --depth 1 "%REPO_HTTPS%" "%TARGET_DIR%" + if errorlevel 1 ( + echo ERROR: Failed to clone repository via HTTPS into %TARGET_DIR%. + endlocal & exit /b 1 + ) ) ) -REM If git-lfs is available, ensure LFS content is pulled -where git >nul 2>nul -if %ERRORLEVEL%==0 ( - pushd "%TARGET_DIR%" +REM If git-lfs is available, ensure LFS content is pulled (ignore failures) +if exist "%TARGET_DIR%\.git" ( + pushd "%TARGET_DIR%" >nul 2>nul git lfs version >nul 2>nul - if %ERRORLEVEL%==0 ( + if not errorlevel 1 ( git lfs install --local >nul 2>nul - git lfs pull + git lfs pull || rem ignore ) - popd + popd >nul 2>nul ) echo Bistro assets ready at: %TARGET_DIR% -endlocal -exit /b 0 +endlocal & exit /b 0 From 11e9a659d49dfb382dc5cf9eabea8a01ac0af091 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:42:46 -0700 Subject: [PATCH 061/102] committing requested changes in stages. --- attachments/simple_engine/debug_system.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h index 5681bea5..3bb9ca9f 100644 --- a/attachments/simple_engine/debug_system.h +++ b/attachments/simple_engine/debug_system.h @@ -195,6 +195,8 @@ class DebugSystem { * @param name The name of the measurement. */ void StopMeasurement(const std::string& name) { + std::lock_guard lock(mutex); + auto now = std::chrono::high_resolution_clock::now(); auto it = measurements.find(name); From 5e6209c8f727f618395b6df3252e1a6df7cc969b Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:44:14 -0700 Subject: [PATCH 062/102] committing requested changes in stages. --- attachments/simple_engine/debug_system.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h index 3bb9ca9f..d1c81b24 100644 --- a/attachments/simple_engine/debug_system.h +++ b/attachments/simple_engine/debug_system.h @@ -195,9 +195,9 @@ class DebugSystem { * @param name The name of the measurement. */ void StopMeasurement(const std::string& name) { + auto now = std::chrono::high_resolution_clock::now(); std::lock_guard lock(mutex); - auto now = std::chrono::high_resolution_clock::now(); auto it = measurements.find(name); if (it != measurements.end()) { From 25e66d50057b4b714561f80e790f47ad392463fb Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:48:52 -0700 Subject: [PATCH 063/102] committing requested changes in stages. --- antora/modules/ROOT/nav.adoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 08155af5..7927901d 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -52,4 +52,13 @@ * xref:15_GLTF_KTX2_Migration.adoc[Migrating to Modern Asset Formats: glTF and KTX2] * xref:16_Multiple_Objects.adoc[Rendering Multiple Objects] * xref:17_Multithreading.adoc[Multithreading] +* xref:Building_a_Simple_Engine/introduction.adoc[Building a Simple Engine] +** xref:Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc[Engine Architecture] +** xref:Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc[Camera Transformations] +** xref:Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc[Lighting & Materials] +** xref:Building_a_Simple_Engine/GUI/01_introduction.adoc[GUI] +** xref:Building_a_Simple_Engine/Loading_Models/01_introduction.adoc[Loading Models] +** xref:Building_a_Simple_Engine/Subsystems/01_introduction.adoc[Subsystems] +** xref:Building_a_Simple_Engine/Tooling/01_introduction.adoc[Tooling] +** xref:Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc[Mobile Development] * xref:90_FAQ.adoc[FAQ] From 5849fa5f0713ad16175c3fab86ea5ccca715adce Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:55:38 -0700 Subject: [PATCH 064/102] switch from link to xref. --- .../Appendix/appendix.adoc | 4 ++-- .../01_introduction.adoc | 10 +++++----- .../04_camera_implementation.adoc | 4 ++-- .../Camera_Transformations/06_conclusion.adoc | 2 +- .../02_architectural_patterns.adoc | 10 +++++----- .../05_rendering_pipeline.adoc | 8 ++++---- .../Engine_Architecture/conclusion.adoc | 4 ++-- .../Loading_Models/01_introduction.adoc | 10 +++++----- .../Loading_Models/05_pbr_rendering.adoc | 6 +++--- .../Mobile_Development/06_conclusion.adoc | 2 +- .../Subsystems/06_conclusion.adoc | 2 +- .../Tooling/07_conclusion.adoc | 2 +- .../Tooling/index.adoc | 18 ++++++++--------- en/Building_a_Simple_Engine/introduction.adoc | 20 +++++++++---------- 14 files changed, 51 insertions(+), 51 deletions(-) diff --git a/en/Building_a_Simple_Engine/Appendix/appendix.adoc b/en/Building_a_Simple_Engine/Appendix/appendix.adoc index c0f3b8f5..e18fb6cc 100644 --- a/en/Building_a_Simple_Engine/Appendix/appendix.adoc +++ b/en/Building_a_Simple_Engine/Appendix/appendix.adoc @@ -335,5 +335,5 @@ When designing your engine architecture, consider: 3. *Team Size and Experience* - More complex architectures may be harder to work with for smaller teams. 4. *Project Scope* - A small project may not need the complexity of a full ECS. -link:../Engine_Architecture/02_architectural_patterns.adoc[Back to Architectural Patterns] -link:../Engine_Architecture/05_rendering_pipeline.adoc[Back to Rendering Pipeline] +xref:../Engine_Architecture/02_architectural_patterns.adoc[Back to Architectural Patterns] +xref:../Engine_Architecture/05_rendering_pipeline.adoc[Back to Rendering Pipeline] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc index e0674b62..02ba0980 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc @@ -22,10 +22,10 @@ By the end of this chapter, you'll have a solid understanding of 3D transformati Before starting this chapter, you should have completed the main Vulkan tutorial. You should also be familiar with: * Basic Vulkan concepts: -** link:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] -** link:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] -* link:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and link:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] -* link:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] * Basic programming concepts and C++ -link:02_math_foundations.adoc[Next: Mathematical Foundations] +xref:02_math_foundations.adoc[Next: Mathematical Foundations] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc index d729a9c3..a1ea6b14 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc @@ -684,9 +684,9 @@ void gameLoop(float deltaTime) { [NOTE] ==== -For more advanced camera techniques, refer to the Advanced Camera Techniques section in the link:../Appendix/appendix.adoc[Appendix]. +For more advanced camera techniques, refer to the Advanced Camera Techniques section in the xref:../Appendix/appendix.adoc[Appendix]. ==== In the next section, we'll integrate our camera system with Vulkan to render 3D scenes. -link:05_vulkan_integration.adoc[Next: Vulkan Integration] +xref:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc index 34017228..d1554b56 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc @@ -52,4 +52,4 @@ A well-designed camera system is essential for any 3D application. It serves as Remember that the code provided in this chapter is a starting point. Feel free to modify and extend it to suit your specific needs and application requirements. -link:../Engine_Architecture/conclusion.adoc[Previous: Engine Architecture] | link:../Lighting_Materials/01_introduction.adoc[Next: Lighting & Materials] +xref:../Engine_Architecture/conclusion.adoc[Previous: Engine Architecture] | xref:../Lighting_Materials/01_introduction.adoc[Next: Lighting & Materials] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index b2e6a395..e09bce33 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -23,7 +23,7 @@ image::../../../images/layered_architecture_diagram.png[Layered Architecture Dia * Easier to understand and maintain * Can replace or modify individual layers without affecting others -For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#layered-architecture[Appendix: Layered Architecture]. +For detailed information and implementation examples, see the xref:../Appendix/appendix.adoc#layered-architecture[Appendix: Layered Architecture]. ==== link:https://www.youtube.com/watch?v=rX0ItVEVjHc[Data-Oriented Design] @@ -36,7 +36,7 @@ image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Dia * More efficient memory usage * Easier to parallelize -For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#data-oriented-design[Appendix: Data-Oriented Design]. +For detailed information and implementation examples, see the xref:../Appendix/appendix.adoc#data-oriented-design[Appendix: Data-Oriented Design]. ==== link:https://gameprogrammingpatterns.com/service-locator.html[Service Locator Pattern] @@ -49,7 +49,7 @@ image::../../../images/service_locator_pattern_diagram.svg[Service Locator Patte * Allows for easy service replacement * Facilitates testing with mock services -For detailed information and implementation examples, see the link:../Appendix/appendix.adoc#service-locator-pattern[Appendix: Service Locator Pattern]. +For detailed information and implementation examples, see the xref:../Appendix/appendix.adoc#service-locator-pattern[Appendix: Service Locator Pattern]. === link:https://gameprogrammingpatterns.com/component.html[Component-Based Architecture] @@ -164,8 +164,8 @@ While other architectural patterns have their merits, component-based architectu === Conclusion -We've provided a brief overview of common architectural patterns, with a focus on Component-Based Architecture which we'll use throughout this tutorial. For more detailed information about other architectural patterns, including implementation examples and comparative analysis, see the link:../Appendix/appendix.adoc[Appendix: Detailed Architectural Patterns]. +We've provided a brief overview of common architectural patterns, with a focus on Component-Based Architecture which we'll use throughout this tutorial. For more detailed information about other architectural patterns, including implementation examples and comparative analysis, see the xref:../Appendix/appendix.adoc[Appendix: Detailed Architectural Patterns]. In the next section, we'll dive deeper into component systems and how to implement them effectively in your engine. -link:01_introduction.adoc[Previous: Introduction] | link:03_component_systems.adoc[Next: Component Systems] +xref:01_introduction.adoc[Previous: Introduction] | xref:03_component_systems.adoc[Next: Component Systems] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 225752c9..77715ee4 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -529,10 +529,10 @@ Proper synchronization is crucial in Vulkan because: The vulkan tutorial includes a more detailed discussion of synchronization, the proper uses of the primitives described above. -* link:../../03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc[Synchronization] -* link:../../03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc[Frames In Flight] -* link:../../11_Compute_Shader.adoc[Compute Shader] -* link:../../17_Multithreading.adoc[Multithreading] +* xref:../../03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc[Synchronization] +* xref:../../03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc[Frames In Flight] +* xref:../../11_Compute_Shader.adoc[Compute Shader] +* xref:../../17_Multithreading.adoc[Multithreading] ===== Pipeline Barriers diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc index 6cdd1be0..f6aa92ea 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc @@ -36,7 +36,7 @@ The architectural foundation we've established in this chapter will support ever Active implementation proves far more valuable than passive reading when learning engine architecture. Build the code examples as you encounter them, but don't stop there—experiment with variations to understand how different approaches affect your engine's behavior. This experimentation develops the intuitive understanding that separates competent engine developers from those who merely copy implementations. -The architectural concepts we've covered provide a foundation, but production engines require additional sophistication. The link:../Appendix/appendix.adoc[Appendix] explores advanced rendering techniques and architectural patterns that build on these fundamentals, helping you understand how simple patterns scale to handle complex real-world requirements. +The architectural concepts we've covered provide a foundation, but production engines require additional sophistication. The xref:../Appendix/appendix.adoc[Appendix] explores advanced rendering techniques and architectural patterns that build on these fundamentals, helping you understand how simple patterns scale to handle complex real-world requirements. Studying existing open-source engines like link:https://github.com/TheCherno/Hazel[Hazel] or examining the architectural decisions in established frameworks like link:https://github.com/LWJGL/lwjgl3[LWJGL] provides valuable perspective on how these concepts apply in practice. Look for patterns we've discussed and notice how different engines make different trade-offs based on their specific goals and constraints. @@ -50,4 +50,4 @@ Building a rendering engine is a challenging but rewarding endeavor. By applying Good luck with your engine development journey! -link:06_event_systems.adoc[Previous: Event Systems] | link:../Camera_Transformations/01_introduction.adoc[Next: Camera Transformations] +xref:06_event_systems.adoc[Previous: Event Systems] | xref:../Camera_Transformations/01_introduction.adoc[Next: Camera Transformations] diff --git a/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc index 876a927c..637c7177 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc @@ -22,12 +22,12 @@ Throughout this implementation, we'll structure our code with engine-level think This chapter builds on the foundation established in the main Vulkan tutorial, particularly Chapter 16 (Multiple Objects), as we'll extend those concepts to handle more complex scene organization and asset management. The multiple objects chapter introduced the basic concepts of rendering different geometry, which we'll now scale up to handle complete 3D models with materials and animations. -You'll need solid familiarity with core Vulkan concepts that form the backbone of our model loading system. link:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] become more complex when handling multiple models with different materials, as we'll need to manage descriptor sets and push constants efficiently. Understanding link:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[graphics pipelines] is crucial since different materials might require different pipeline configurations. +You'll need solid familiarity with core Vulkan concepts that form the backbone of our model loading system. xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] become more complex when handling multiple models with different materials, as we'll need to manage descriptor sets and push constants efficiently. Understanding xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[graphics pipelines] is crucial since different materials might require different pipeline configurations. -Experience with link:../../04_Vertex_buffers/00_Vertex_input_description.adoc[vertex] and link:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] translates directly to model loading, where glTF files contain vertex data in specific formats that we'll need to parse and upload to GPU buffers. link:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] knowledge becomes essential as we'll use them for transformation matrices, lighting information, and material properties. +Experience with xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] translates directly to model loading, where glTF files contain vertex data in specific formats that we'll need to parse and upload to GPU buffers. xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] knowledge becomes essential as we'll use them for transformation matrices, lighting information, and material properties. -link:../../06_Texture_mapping/00_Images.adoc[Texture mapping] skills are particularly important since glTF models often include multiple textures per material (diffuse, normal, metallic-roughness, etc.), and we'll need to load and bind these textures efficiently. +xref:../../06_Texture_mapping/00_Images.adoc[Texture mapping] skills are particularly important since glTF models often include multiple textures per material (diffuse, normal, metallic-roughness, etc.), and we'll need to load and bind these textures efficiently. -Finally, basic 3D math understanding (matrices, vectors, quaternions) is crucial for handling model transformations, animations, and scene hierarchies. If you need a refresher, see the link:../../Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc[Camera Transformations chapter] for detailed coverage of these mathematical concepts. +Finally, basic 3D math understanding (matrices, vectors, quaternions) is crucial for handling model transformations, animations, and scene hierarchies. If you need a refresher, see the xref:../../Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc[Camera Transformations chapter] for detailed coverage of these mathematical concepts. -link:../GUI/06_conclusion.adoc[Previous: GUI] | link:02_project_setup.adoc[Next: Setting Up the Project] +xref:../GUI/06_conclusion.adoc[Previous: GUI] | xref:02_project_setup.adoc[Next: Setting Up the Project] diff --git a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc index 4150edb5..c768e5d3 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc @@ -6,7 +6,7 @@ === Building on PBR Knowledge -In the link:../Lighting_Materials/01_introduction.adoc[Lighting & Materials chapter], we explored the fundamentals of Physically Based Rendering (PBR), including its core principles, the BRDF, and material properties. Now, we'll apply that knowledge to implement a PBR pipeline for the glTF models we've loaded. +In the xref:../Lighting_Materials/01_introduction.adoc[Lighting & Materials chapter], we explored the fundamentals of Physically Based Rendering (PBR), including its core principles, the BRDF, and material properties. Now, we'll apply that knowledge to implement a PBR pipeline for the glTF models we've loaded. As we learned in the link:../../15_GLTF_KTX2_Migration.html[glTF and KTX2 Migration chapter], glTF uses PBR with the metallic-roughness workflow for its material system. This aligns perfectly with the PBR concepts we've already covered, making it straightforward to render our glTF models with physically accurate lighting. @@ -61,7 +61,7 @@ This uniform buffer includes: [NOTE] ==== -We introduced push constants earlier in link:../Lighting_Materials/03_push_constants.adoc[push constants]; here we focus on how the same mechanism carries glTF metallic‑roughness material knobs efficiently per draw. +We introduced push constants earlier in xref:../Lighting_Materials/03_push_constants.adoc[push constants]; here we focus on how the same mechanism carries glTF metallic‑roughness material knobs efficiently per draw. ==== We'll use link:https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#descriptorsets-pushconstant[push constants] to pass material properties to the shader: @@ -571,4 +571,4 @@ In the next chapter, we'll explore how to render multiple objects with different If you want to dive deeper into lighting and materials, refer back to the Lighting & Materials chapter, where we explored the theory behind PBR in detail. -link:04_loading_gltf.adoc[Previous: Loading a glTF Model] | link:06_multiple_objects.adoc[Next: Rendering Multiple Objects] +xref:04_loading_gltf.adoc[Previous: Loading a glTF Model] | xref:06_multiple_objects.adoc[Next: Rendering Multiple Objects] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc index a002f05a..265a7976 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -266,4 +266,4 @@ link:../../attachments/simple_engine/37_mobile_optimizations.cpp[Mobile Optimiza link:../../attachments/simple_engine/38_tbr_optimizations.cpp[TBR Optimizations C{pp} code] link:../../attachments/simple_engine/39_mobile_extensions.cpp[Mobile Extensions C{pp} code] -link:05_vulkan_extensions.adoc[Previous: Vulkan Extensions for Mobile] | link:../index.html[Back to Building a Simple Engine] +xref:05_vulkan_extensions.adoc[Previous: Vulkan Extensions for Mobile] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc index 6e3fd900..f83f88ca 100644 --- a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc @@ -108,4 +108,4 @@ The complete code for this chapter can be found in the following files: link:../../attachments/simple_engine/30_audio_subsystem.cpp[Audio Subsystem C{pp} code] link:../../attachments/simple_engine/31_physics_subsystem.cpp[Physics Subsystem C{pp} code] -link:05_vulkan_physics.adoc[Previous: Vulkan for Physics Simulation] | link:../Tooling/01_introduction.adoc[Next: Tooling] | link:../index.html[Back to Building a Simple Engine] +xref:05_vulkan_physics.adoc[Previous: Vulkan for Physics Simulation] | xref:../Tooling/01_introduction.adoc[Next: Tooling] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc index f5c4451b..91a36b6d 100644 --- a/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc @@ -229,4 +229,4 @@ link:../../attachments/simple_engine/33_debug_utils.cpp[Debug Utils C{pp} code] link:../../attachments/simple_engine/34_crash_handling.cpp[Crash Handling C{pp} code] link:../../attachments/simple_engine/35_robustness_extensions.cpp[Robustness Extensions C{pp} code] -link:06_packaging_and_distribution.adoc[Previous: Packaging and Distribution] | link:../Mobile_Development/01_introduction.adoc[Next: Mobile Development] +xref:06_packaging_and_distribution.adoc[Previous: Packaging and Distribution] | xref:../Mobile_Development/01_introduction.adoc[Next: Mobile Development] diff --git a/en/Building_a_Simple_Engine/Tooling/index.adoc b/en/Building_a_Simple_Engine/Tooling/index.adoc index 511903ab..25d26369 100644 --- a/en/Building_a_Simple_Engine/Tooling/index.adoc +++ b/en/Building_a_Simple_Engine/Tooling/index.adoc @@ -4,12 +4,12 @@ This chapter covers essential tooling and techniques for developing, debugging, and distributing Vulkan applications, with a focus on using modern C++20 modules and the vk::raii namespace. -* link:01_introduction.adoc[Introduction] -* link:02_cicd.adoc[CI/CD for Vulkan Projects] -* link:03_debugging_and_renderdoc.adoc[Debugging with VK_KHR_debug_utils and RenderDoc] -* link:04_crash_minidump.adoc[Crash Handling and Minidumps] -* link:05_extensions.adoc[Vulkan Extensions for Robustness] -* link:06_packaging_and_distribution.adoc[Packaging and Distribution] -* link:07_conclusion.adoc[Conclusion] - -link:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:../index.html[Back to Building a Simple Engine] +* xref:01_introduction.adoc[Introduction] +* xref:02_cicd.adoc[CI/CD for Vulkan Projects] +* xref:03_debugging_and_renderdoc.adoc[Debugging with VK_KHR_debug_utils and RenderDoc] +* xref:04_crash_minidump.adoc[Crash Handling and Minidumps] +* xref:05_extensions.adoc[Vulkan Extensions for Robustness] +* xref:06_packaging_and_distribution.adoc[Packaging and Distribution] +* xref:07_conclusion.adoc[Conclusion] + +xref:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc index 9909d854..16c3b90e 100644 --- a/en/Building_a_Simple_Engine/introduction.adoc +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -10,7 +10,7 @@ Welcome to the "Building a Simple Engine" tutorial series! This series marks a t While the previous tutorial series focused on introducing individual Vulkan concepts step by step, this series takes a different approach: -This series targets readers who have completed the link:../00_Introduction.adoc[Vulkan Tutorial] and feel comfortable with the fundamentals. We’ll emphasize architectural concepts and design patterns over exhaustive API permutations, so you develop an engine mindset rather than a collection of snippets. Expect to do more independent work: fill in smaller gaps, experiment, and lean on the https://docs.vulkan.org/guide/latest/[Vulkan Guide], https://docs.vulkan.org/samples/latest/[Samples], and https://docs.vulkan.org/spec/latest/[Specification] as primary references. If a topic feels too advanced, revisit the original tutorial and return when ready. +This series targets readers who have completed the xref:../00_Introduction.adoc[Vulkan Tutorial] and feel comfortable with the fundamentals. We’ll emphasize architectural concepts and design patterns over exhaustive API permutations, so you develop an engine mindset rather than a collection of snippets. Expect to do more independent work: fill in smaller gaps, experiment, and lean on the https://docs.vulkan.org/guide/latest/[Vulkan Guide], https://docs.vulkan.org/samples/latest/[Samples], and https://docs.vulkan.org/spec/latest/[Specification] as primary references. If a topic feels too advanced, revisit the original tutorial and return when ready. === What to Expect @@ -36,16 +36,16 @@ Each chapter builds on the last to assemble a small but capable engine. Read a c Let's begin our journey into engine development with these chapters: -1. link:Engine_Architecture/01_introduction.adoc[Engine Architecture] - How to structure your code for flexibility, maintainability, and extensibility. -2. link:Camera_Transformations/01_introduction.adoc[Camera Transformations] - Implementation of camera systems and transformations. -3. link:Lighting_Materials/01_introduction.adoc[Lighting & Materials] - Basic lighting models and push constants. -4. link:GUI/01_introduction.adoc[GUI] - Implementation of a graphical user interface using Dear ImGui. -5. link:Loading_Models/01_introduction.adoc[Loading Models] - More sophisticated approaches to handling models, textures, and other assets. -6. link:Subsystems/01_introduction.adoc[Subsystems] - Implementation of Audio and Physics subsystems with Vulkan compute capabilities. -7. link:Tooling/01_introduction.adoc[Tooling] - CI/CD, Debugging, Crash minidump, Distribution, and Vulkan extensions for robustness. -8. link:Mobile_Development/01_introduction.adoc[Mobile Development] - Adapting the engine for Android/iOS, focusing on performance considerations and mobile-specific Vulkan extensions. +1. xref:Engine_Architecture/01_introduction.adoc[Engine Architecture] - How to structure your code for flexibility, maintainability, and extensibility. +2. xref:Camera_Transformations/01_introduction.adoc[Camera Transformations] - Implementation of camera systems and transformations. +3. xref:Lighting_Materials/01_introduction.adoc[Lighting & Materials] - Basic lighting models and push constants. +4. xref:GUI/01_introduction.adoc[GUI] - Implementation of a graphical user interface using Dear ImGui. +5. xref:Loading_Models/01_introduction.adoc[Loading Models] - More sophisticated approaches to handling models, textures, and other assets. +6. xref:Subsystems/01_introduction.adoc[Subsystems] - Implementation of Audio and Physics subsystems with Vulkan compute capabilities. +7. xref:Tooling/01_introduction.adoc[Tooling] - CI/CD, Debugging, Crash minidump, Distribution, and Vulkan extensions for robustness. +8. xref:Mobile_Development/01_introduction.adoc[Mobile Development] - Adapting the engine for Android/iOS, focusing on performance considerations and mobile-specific Vulkan extensions. -link:../conclusion.adoc[Previous: Main Tutorial Conclusion] | link:Engine_Architecture/01_introduction.adoc[Next: Engine Architecture] +xref:../conclusion.adoc[Previous: Main Tutorial Conclusion] | xref:Engine_Architecture/01_introduction.adoc[Next: Engine Architecture] === Getting Started with Example Assets From f1df3d2b69e83349dc419eb3f568dca59d2d4672 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:57:51 -0700 Subject: [PATCH 065/102] addressing more comments. --- .../Camera_Transformations/05_vulkan_integration.adoc | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc index b8919669..878431b1 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -12,8 +12,6 @@ Before we dive into the integration, let's briefly introduce the key libraries w * *GLM* (OpenGL Mathematics): A mathematics library for graphics programming that provides vector and matrix operations similar to GLSL. We use it for all our 3D math operations. [https://github.com/g-truc/glm] -* *Vulkan*: The low-level graphics API we're using for rendering. [https://www.vulkan.org/] - Now that we have a camera system and understand transformation matrices, let's integrate them with our Vulkan application. We'll focus on how to set up uniform buffers for our matrices and update them each frame based on camera movement. To keep the integration digestible, think of it in five small steps: @@ -36,9 +34,9 @@ First, we need to define our uniform buffer structure: [source,cpp] ---- struct UniformBufferObject { - alignas(16) glm::mat4 model; - alignas(16) glm::mat4 view; - alignas(16) glm::mat4 proj; + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; }; ---- From 7eaa47eb0ad7bd53e8340376074eab295112791b Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 14:04:12 -0700 Subject: [PATCH 066/102] addressing more comments. --- .../05_vulkan_integration.adoc | 53 +++++++++++++------ .../04_lighting_implementation.adoc | 4 +- .../Loading_Models/05_pbr_rendering.adoc | 12 ++--- .../Loading_Models/06_multiple_objects.adoc | 8 +-- .../Loading_Models/07_scene_rendering.adoc | 4 +- 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc index 878431b1..2f0e8576 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -42,15 +42,35 @@ struct UniformBufferObject { Next, we'll create the uniform buffer and its descriptor set: +[NOTE] +==== +Uniform buffers should be allocated per frame-in-flight (maxConcurrentFrames), not per swapchain image. This matches how you submit work and synchronize frames, avoids unnecessary allocations, and simplifies your logic. +==== + [source,cpp] ---- +// Use a fixed number of frames-in-flight, not the number of swapchain images +constexpr uint32_t maxConcurrentFrames = 2; // Adjust to your renderer + +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +// Keep the mapped pointer alongside the buffer for clarity and safety +struct UboBuffer { + vk::raii::Buffer buffer{nullptr}; + vk::raii::DeviceMemory memory{nullptr}; + void* mapped = nullptr; +}; + +std::array uniformBuffers; + void createUniformBuffers() { vk::DeviceSize bufferSize = sizeof(UniformBufferObject); - uniformBuffers.resize(swapChainImages.size()); - uniformBuffersMapped.resize(swapChainImages.size()); - - for (size_t i = 0; i < swapChainImages.size(); i++) { + for (size_t i = 0; i < maxConcurrentFrames; i++) { // Create the buffer vk::BufferCreateInfo bufferInfo{ .size = bufferSize, @@ -58,10 +78,10 @@ void createUniformBuffers() { .sharingMode = vk::SharingMode::eExclusive }; - uniformBuffers[i] = vk::raii::Buffer(device, bufferInfo); + uniformBuffers[i].buffer = vk::raii::Buffer(device, bufferInfo); // Allocate and bind memory - vk::MemoryRequirements memRequirements = uniformBuffers[i].getMemoryRequirements(); + vk::MemoryRequirements memRequirements = uniformBuffers[i].buffer.getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo{ .allocationSize = memRequirements.size, @@ -71,11 +91,11 @@ void createUniformBuffers() { ) }; - uniformBuffersMemory[i] = vk::raii::DeviceMemory(device, allocInfo); - uniformBuffers[i].bindMemory(*uniformBuffersMemory[i], 0); + uniformBuffers[i].memory = vk::raii::DeviceMemory(device, allocInfo); + uniformBuffers[i].buffer.bindMemory(*uniformBuffers[i].memory, 0); // Persistently map the buffer memory - uniformBuffersMapped[i] = uniformBuffersMemory[i].mapMemory(0, bufferSize); + uniformBuffers[i].mapped = uniformBuffers[i].memory.mapMemory(0, bufferSize); } } ---- @@ -111,19 +131,20 @@ Now we'll create descriptor sets that point to our uniform buffers: [source,cpp] ---- void createDescriptorSets() { - std::vector layouts(swapChainImages.size(), *descriptorSetLayout); + std::array layouts{}; + layouts.fill(*descriptorSetLayout); vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, - .descriptorSetCount = static_cast(swapChainImages.size()), + .descriptorSetCount = maxConcurrentFrames, .pSetLayouts = layouts.data() }; descriptorSets = device.allocateDescriptorSets(allocInfo); - for (size_t i = 0; i < swapChainImages.size(); i++) { + for (size_t i = 0; i < maxConcurrentFrames; i++) { vk::DescriptorBufferInfo bufferInfo{ - .buffer = *uniformBuffers[i], + .buffer = *uniformBuffers[i].buffer, .offset = 0, .range = sizeof(UniformBufferObject) }; @@ -148,7 +169,7 @@ In our main loop, we'll update the uniform buffer with the latest camera data: [source,cpp] ---- -void updateUniformBuffer(uint32_t currentImage) { +void updateUniformBuffer(uint32_t currentFrame) { static auto startTime = std::chrono::high_resolution_clock::now(); auto currentTime = std::chrono::high_resolution_clock::now(); float time = std::chrono::duration(currentTime - startTime).count(); @@ -167,8 +188,8 @@ void updateUniformBuffer(uint32_t currentImage) { // Vulkan's Y coordinate is inverted compared to OpenGL ubo.proj[1][1] *= -1; - // Copy the data to the uniform buffer - memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + // Copy the data to the uniform buffer for the current frame-in-flight + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); } ---- diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc index 723a3c60..1149e64c 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc @@ -691,7 +691,7 @@ This function creates a new pipeline for our PBR shader, including support for p [source,cpp] ---- // Update uniform buffer -void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera) { +void Renderer::updateUniformBuffer(uint32_t currentFrame, Entity* entity, CameraComponent* camera) { // Get the transform component from the entity auto transform = entity->GetComponent(); if (!transform) { @@ -744,7 +744,7 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera // Copy the uniform buffer object to the device memory using vk::raii // With vk::raii, we can use the mapped memory directly - memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); } ---- diff --git a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc index c768e5d3..e9cccceb 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc @@ -408,7 +408,7 @@ void setupLights() { }; // Update uniform buffer with light data - for (size_t i = 0; i < swapChainImages.size(); i++) { + for (size_t i = 0; i < maxConcurrentFrames; i++) { UniformBufferObject ubo{}; // ... (set up transformation matrices) @@ -425,8 +425,8 @@ void setupLights() { ubo.exposure = 4.5f; ubo.gamma = 2.2f; - // Copy to uniform buffer - memcpy(uniformBuffersMapped[i], &ubo, sizeof(ubo)); + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[i].mapped, &ubo, sizeof(ubo)); } } ---- @@ -437,7 +437,7 @@ PBR relies on view-dependent effects like the Fresnel effect, so we need to inte [source,cpp] ---- -void updateUniformBuffer(uint32_t currentImage) { +void updateUniformBuffer(uint32_t currentFrame) { UniformBufferObject ubo{}; // Update transformation matrices @@ -453,8 +453,8 @@ void updateUniformBuffer(uint32_t currentImage) { // ... (update other PBR parameters) - // Copy to uniform buffer - memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); } ---- diff --git a/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc index e57d7efb..c5942ec3 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc @@ -234,8 +234,8 @@ void drawFrame() { ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted - // Copy to uniform buffer - memcpy(uniformBuffersMapped[currentFrame], &ubo, sizeof(ubo)); + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); // Render each object instance for (size_t i = 0; i < objectInstances.size(); i++) { @@ -535,8 +535,8 @@ void drawFrame() { ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted - // Copy to uniform buffer - memcpy(uniformBuffersMapped[currentFrame], &ubo, sizeof(ubo)); + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); // Bind descriptor set commandBuffer.bindDescriptorSets( diff --git a/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc index b20cfb34..0e0dacf1 100644 --- a/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc +++ b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc @@ -357,8 +357,8 @@ void drawFrame() { ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted - // Copy to uniform buffer - memcpy(uniformBuffersMapped[currentFrame], &ubo, sizeof(ubo)); + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); // Render the scene renderScene(commandBuffer, model, ubo.view, ubo.proj); From 92c7f7dd4188ec2efe750241b77c338c2b1cc1d7 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 14:13:57 -0700 Subject: [PATCH 067/102] addressing more comments. --- .../GUI/04_ui_elements.adoc | 4 +- .../02_lighting_models.adoc | 6 +- .../05_vulkan_integration.adoc | 276 +----------------- 3 files changed, 8 insertions(+), 278 deletions(-) diff --git a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc index 6706e8c9..3b619480 100644 --- a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc +++ b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc @@ -88,13 +88,13 @@ ImGui requires descriptors for its font texture. Ensure your descriptor pool has ---- // Create descriptor pool with enough capacity for ImGui vk::DescriptorPoolSize poolSizes[] = { - { vk::DescriptorType::eCombinedImageSampler, 1000 }, + { vk::DescriptorType::eCombinedImageSampler, 50 }, // Other descriptor types... }; vk::DescriptorPoolCreateInfo poolInfo{}; poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; -poolInfo.maxSets = 1000; +poolInfo.maxSets = 50; poolInfo.poolSizeCount = static_cast(std::size(poolSizes)); poolInfo.pPoolSizes = poolSizes; diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc index 2e9d57e7..c726feb0 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc @@ -60,7 +60,7 @@ One of the most widely used traditional lighting models, developed by Bui Tuong * *Disadvantages*: Not physically accurate, can look artificial under certain lighting conditions * *When to use*: For simple real-time applications where PBR is too expensive -For more information on the Phong lighting model, see the link:https://en.wikipedia.org/wiki/Phong_reflection_model[Wikipedia article] or this link:https://www.cs.utah.edu/~shirley/books/fcg2/rt.pdf[computer graphics textbook]. +For more information on the Phong lighting model, see the link:https://en.wikipedia.org/wiki/Phong_reflection_model[Wikipedia article]. ==== Blinn-Phong Model @@ -149,7 +149,7 @@ Global Illumination (GI) simulates how light bounces between surfaces, creating * *Path Tracing*: Traces light paths through the scene * *Photon Mapping*: Stores light information in a spatial data structure -For more information, see this link:https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-12-tricks-real-time-radiosity[GPU Gems chapter on radiosity]. +For more information, see this link:https://developer.download.nvidia.com/books/HTML/gpugems2/chapters/gpugems2_chapter12.html[GPU Gems chapter on radiosity]. === Subsurface Scattering @@ -161,7 +161,7 @@ For more information, see this link:https://developer.nvidia.com/gpugems/gpugems Ambient Occlusion (AO) approximates how much ambient light a surface point would receive, darkening corners and crevices. -For more information, see this link:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-17-ambient-occlusion[GPU Gems chapter on ambient occlusion]. +For more information, see this link:https://developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch17.html[GPU Gems chapter on ambient occlusion]. == Choosing the Right Lighting Model diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc index 8bdbffe0..c11378cc 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc @@ -144,280 +144,10 @@ void Renderer::recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t ima } ---- -== Creating the PBR Shader +== PBR Shader Reference -Now that we've updated our renderer to support our PBR implementation, we need to create the PBR shader that implements the concepts we've discussed in this chapter. Rather than presenting this as a monolithic code dump, let's break down the shader creation into logical sections that explain both the technical implementation and the reasoning behind each component. +This chapter reuses the exact PBR shader defined in the previous section to avoid duplication and drift. Please refer to link:04_lighting_implementation.adoc[Implementing the PBR Shader] for the full pbr.slang source and detailed explanations. Here we focus strictly on Vulkan integration: pipeline layout, descriptor bindings, push constants, and draw submission. -=== Section 1: Shader Structure and Data Interface - -The first section establishes the communication interface between our CPU application and GPU shader, defining how data flows through the rendering pipeline. - -[source,cpp] ----- -// Combined vertex and fragment shader for PBR rendering - -// Input from vertex buffer - Data sent per vertex from CPU -struct VSInput { - float3 Position : POSITION; // 3D position in model space - float3 Normal : NORMAL; // Surface normal for lighting calculations - float2 UV : TEXCOORD0; // Texture coordinates for material sampling - float4 Tangent : TANGENT; // Tangent vector for normal mapping (w component = handedness) -}; - -// Output from vertex shader / Input to fragment shader - Interpolated data -struct VSOutput { - float4 Position : SV_POSITION; // Required clip space position for rasterization - float3 WorldPos : POSITION; // World space position for lighting calculations - float3 Normal : NORMAL; // World space normal (interpolated) - float2 UV : TEXCOORD0; // Texture coordinates (interpolated) - float4 Tangent : TANGENT; // World space tangent (interpolated) -}; - -// Uniform buffer - Global data shared across all vertices/fragments -struct UniformBufferObject { - float4x4 model; // Model-to-world transformation matrix - float4x4 view; // World-to-camera transformation matrix - float4x4 proj; // Camera-to-clip space projection matrix - float4 lightPositions[4]; // Light positions in world space - float4 lightColors[4]; // Light intensities and colors - float4 camPos; // Camera position for view-dependent effects - float exposure; // HDR exposure control - float gamma; // Gamma correction value (typically 2.2) - float prefilteredCubeMipLevels; // IBL prefiltered environment map mip levels - float scaleIBLAmbient; // IBL ambient contribution scale -}; - -// Push constants - Fast, small data updated frequently per material/object -struct PushConstants { - float4 baseColorFactor; // Base color tint/multiplier - float metallicFactor; // Metallic property multiplier - float roughnessFactor; // Surface roughness multiplier - int baseColorTextureSet; // Texture binding index for base color (-1 = none) - int physicalDescriptorTextureSet; // Texture binding for metallic/roughness - int normalTextureSet; // Texture binding for normal maps - int occlusionTextureSet; // Texture binding for ambient occlusion - int emissiveTextureSet; // Texture binding for emissive maps - float alphaMask; // Alpha masking enable flag - float alphaMaskCutoff; // Alpha cutoff threshold -}; - -// Mathematical constants -static const float PI = 3.14159265359; - -// Resource bindings - Connect CPU resources to GPU shader registers -[[vk::binding(0, 0)]] ConstantBuffer ubo; -[[vk::binding(1, 0)]] Texture2D baseColorMap; -[[vk::binding(1, 0)]] SamplerState baseColorSampler; -[[vk::binding(2, 0)]] Texture2D metallicRoughnessMap; -[[vk::binding(2, 0)]] SamplerState metallicRoughnessSampler; -[[vk::binding(3, 0)]] Texture2D normalMap; -[[vk::binding(3, 0)]] SamplerState normalSampler; -[[vk::binding(4, 0)]] Texture2D occlusionMap; -[[vk::binding(4, 0)]] SamplerState occlusionSampler; -[[vk::binding(5, 0)]] Texture2D emissiveMap; -[[vk::binding(5, 0)]] SamplerState emissiveSampler; - -[[vk::push_constant]] PushConstants material; ----- - -This interface design reflects modern GPU architecture principles where different types of data flow through different pathways based on their update frequency and size. Uniform buffers efficiently handle large, infrequently changing data like transformation matrices, while push constants provide ultra-fast updates for small, frequently changing material properties. - -=== Section 2: PBR Mathematical Foundation - -The second section implements the core mathematical functions that form the foundation of physically-based rendering, translating complex light-surface interactions into computationally efficient approximations. - -[source,cpp] ----- -// Normal Distribution Function (D) - GGX/Trowbridge-Reitz Distribution -// Describes the statistical distribution of microfacet orientations -float DistributionGGX(float NdotH, float roughness) { - float a = roughness * roughness; // Remapping for more perceptual linearity - float a2 = a * a; - float NdotH2 = NdotH * NdotH; - - float nom = a2; // Numerator: concentration factor - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; // Normalization factor - - return nom / denom; // Normalized distribution -} - -// Geometry Function (G) - Smith's method with Schlick-GGX approximation -// Models self-shadowing and masking between microfacets -float GeometrySmith(float NdotV, float NdotL, float roughness) { - float r = roughness + 1.0; - float k = (r * r) / 8.0; // Direct lighting remapping - - // Geometry obstruction from view direction (masking) - float ggx1 = NdotV / (NdotV * (1.0 - k) + k); - // Geometry obstruction from light direction (shadowing) - float ggx2 = NdotL / (NdotL * (1.0 - k) + k); - - return ggx1 * ggx2; // Combined masking-shadowing -} - -// Fresnel Reflectance (F) - Schlick's approximation -// Models how reflectance changes with viewing angle -float3 FresnelSchlick(float cosTheta, float3 F0) { - return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); -} ----- - -These mathematical functions represent decades of computer graphics research distilled into efficient real-time approximations. The GGX distribution provides more realistic highlight falloff compared to older models, while the Smith geometry function ensures energy conservation at grazing angles. The Fresnel approximation captures the essential angle-dependent reflection behavior that makes materials look convincing under different viewing conditions. - -=== Section 3: Vertex and Fragment Shader Implementation - -The final section contains the actual shader entry points that execute for each vertex and pixel, implementing the complete PBR pipeline from geometry transformation through final color output. - -[source,cpp] ----- -// Vertex shader entry point - Executes once per vertex -[[shader("vertex")]] -VSOutput VSMain(VSInput input) -{ - VSOutput output; - - // Transform vertex position through the rendering pipeline - // Model -> World -> Camera -> Clip space transformation chain - float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); - output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); - - // Pass world position for fragment lighting calculations - // Fragment shader needs world space position to calculate light vectors - output.WorldPos = worldPos.xyz; - - // Transform normal from model space to world space - // Use only rotation/scale part of model matrix (upper-left 3x3) - // Normalize to ensure unit length after transformation - output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); - - // Pass through texture coordinates unchanged - // UV coordinates are typically in [0,1] range and don't need transformation - output.UV = input.UV; - - // Pass tangent vector for normal mapping - // Will be used in fragment shader to construct tangent-space basis - output.Tangent = input.Tangent; - - return output; -} - -// Fragment shader entry point - Executes once per pixel -[[shader("fragment")]] -float4 PSMain(VSOutput input) : SV_TARGET -{ - // === MATERIAL PROPERTY SAMPLING === - // Sample base color texture and apply material color factor - float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; - - // Sample metallic-roughness texture (metallic=B channel, roughness=G channel) - // glTF standard: metallic stored in blue, roughness in green - float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; - float metallic = metallicRoughness.x * material.metallicFactor; - float roughness = metallicRoughness.y * material.roughnessFactor; - - // Sample ambient occlusion (typically stored in red channel) - float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; - - // Sample emissive texture for self-illuminating materials - float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; - - // === NORMAL CALCULATION === - // Start with interpolated surface normal - float3 N = normalize(input.Normal); - - // Apply normal mapping if texture is available - if (material.normalTextureSet >= 0) { - // Sample normal map and convert from [0,1] to [-1,1] range - float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; - - // Construct tangent-space to world-space transformation matrix (TBN) - float3 T = normalize(input.Tangent.xyz); // Tangent - float3 B = normalize(cross(N, T)) * input.Tangent.w; // Bitangent (w = handedness) - float3x3 TBN = float3x3(T, B, N); // Tangent-Bitangent-Normal matrix - - // Transform normal from tangent space to world space - N = normalize(mul(tangentNormal, TBN)); - } - - // === LIGHTING SETUP === - // Calculate view direction (camera to fragment) - float3 V = normalize(ubo.camPos.xyz - input.WorldPos); - - // Calculate reflection vector for environment mapping - float3 R = reflect(-V, N); - - // === PBR MATERIAL SETUP === - // Calculate F0 (reflectance at normal incidence) - // Non-metals: low reflectance (~0.04), Metals: colored reflectance from base color - float3 F0 = float3(0.04, 0.04, 0.04); // Dielectric default - F0 = lerp(F0, baseColor.rgb, metallic); // Lerp to metallic behavior - - // Initialize outgoing radiance accumulator - float3 Lo = float3(0.0, 0.0, 0.0); - - // === DIRECT LIGHTING LOOP === - // Calculate contribution from each light source - for (int i = 0; i < 4; i++) { - float3 lightPos = ubo.lightPositions[i].xyz; - float3 lightColor = ubo.lightColors[i].rgb; - - // Calculate light direction and attenuation - float3 L = normalize(lightPos - input.WorldPos); // Light direction - float distance = length(lightPos - input.WorldPos); // Distance for falloff - float attenuation = 1.0 / (distance * distance); // Inverse square falloff - float3 radiance = lightColor * attenuation; // Attenuated light color - - // Calculate half vector (between view and light directions) - float3 H = normalize(V + L); - - // === BRDF EVALUATION === - // Calculate all necessary dot products for BRDF terms - float NdotL = max(dot(N, L), 0.0); // Lambertian falloff - float NdotV = max(dot(N, V), 0.0); // View angle - float NdotH = max(dot(N, H), 0.0); // Half vector for specular - float HdotV = max(dot(H, V), 0.0); // For Fresnel calculation - - // Evaluate Cook-Torrance BRDF components - float D = DistributionGGX(NdotH, roughness); // Normal distribution - float G = GeometrySmith(NdotV, NdotL, roughness); // Geometry function - float3 F = FresnelSchlick(HdotV, F0); // Fresnel reflectance - - // Calculate specular BRDF - float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; // Prevent division by zero - float3 specular = numerator / denominator; - - // === ENERGY CONSERVATION === - // Fresnel term represents specular reflection ratio - float3 kS = F; // Specular contribution - float3 kD = float3(1.0, 1.0, 1.0) - kS; // Diffuse contribution (energy conservation) - kD *= 1.0 - metallic; // Metals have no diffuse reflection - - // === RADIANCE ACCUMULATION === - // Combine diffuse (Lambertian) and specular (Cook-Torrance) terms - // Multiply by incident radiance and cosine foreshortening - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; - } - - // === AMBIENT AND EMISSIVE === - // Add simple ambient lighting (should be replaced with IBL in production) - float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; - - // Combine all lighting contributions - float3 color = ambient + Lo + emissive; - - // === HDR TONE MAPPING AND GAMMA CORRECTION === - // Apply Reinhard tone mapping to compress HDR values to [0,1] range - color = color / (color + float3(1.0, 1.0, 1.0)); - - // Apply gamma correction for sRGB display (inverse gamma) - color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); - - // Output final color with original alpha - return float4(color, baseColor.a); -} ----- == Compiling the Shader @@ -425,7 +155,7 @@ After creating the shader file, we need to compile it using slangc. This is typi [source,bash] ---- -slangc shaders/pbr.slang -target spirv -profile spirv_1_4 -emit-spirv-directly -o shaders/pbr.spv +slangc shaders/pbr.slang -target spirv -profile spirv_1_4 -o shaders/pbr.spv ---- == Testing the Implementation with glTF Models From 625eb719f38ced412b800a8dfc8317725eae219f Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:02:45 -0700 Subject: [PATCH 068/102] addressing more comments. --- attachments/simple_engine/imgui_system.cpp | 83 ++++++++++++------- attachments/simple_engine/imgui_system.h | 17 ++-- attachments/simple_engine/renderer.h | 3 + .../simple_engine/renderer_rendering.cpp | 2 +- 4 files changed, 64 insertions(+), 41 deletions(-) diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 206b6654..5c2d21ff 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -51,6 +51,23 @@ bool ImGuiSystem::Initialize(Renderer* renderer, uint32_t width, uint32_t height return false; } + // Initialize per-frame buffers containers + if (renderer) { + uint32_t frames = renderer->GetMaxFramesInFlight(); + vertexBuffers.clear(); vertexBuffers.reserve(frames); + vertexBufferMemories.clear(); vertexBufferMemories.reserve(frames); + indexBuffers.clear(); indexBuffers.reserve(frames); + indexBufferMemories.clear(); indexBufferMemories.reserve(frames); + for (uint32_t i = 0; i < frames; ++i) { + vertexBuffers.emplace_back(nullptr); + vertexBufferMemories.emplace_back(nullptr); + indexBuffers.emplace_back(nullptr); + indexBufferMemories.emplace_back(nullptr); + } + vertexCounts.assign(frames, 0); + indexCounts.assign(frames, 0); + } + initialized = true; return true; } @@ -368,7 +385,7 @@ void ImGuiSystem::NewFrame() { ImGui::End(); } -void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer) { +void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer, uint32_t frameIndex) { if (!initialized) { return; } @@ -377,8 +394,8 @@ void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer) { // End the frame and prepare for rendering ImGui::Render(); - // Update vertex and index buffers - updateBuffers(); + // Update vertex and index buffers for this frame + updateBuffers(frameIndex); // Record rendering commands ImDrawData* drawData = ImGui::GetDrawData(); @@ -411,11 +428,11 @@ void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer) { commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, pushConstBlock); - // Bind vertex and index buffers - std::array vertexBuffers = {*vertexBuffer}; + // Bind vertex and index buffers for this frame + std::array vertexBuffersArr = {*vertexBuffers[frameIndex]}; std::array offsets = {}; - commandBuffer.bindVertexBuffers(0, vertexBuffers, offsets); - commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint16); + commandBuffer.bindVertexBuffers(0, vertexBuffersArr, offsets); + commandBuffer.bindIndexBuffer(*indexBuffers[frameIndex], 0, vk::IndexType::eUint16); // Render command lists int vertexOffset = 0; @@ -906,7 +923,7 @@ bool ImGuiSystem::createPipeline() { } } -void ImGuiSystem::updateBuffers() { +void ImGuiSystem::updateBuffers(uint32_t frameIndex) { ImDrawData* drawData = ImGui::GetDrawData(); if (!drawData || drawData->CmdListsCount == 0) { return; @@ -919,11 +936,13 @@ void ImGuiSystem::updateBuffers() { vk::DeviceSize vertexBufferSize = drawData->TotalVtxCount * sizeof(ImDrawVert); vk::DeviceSize indexBufferSize = drawData->TotalIdxCount * sizeof(ImDrawIdx); - // Resize buffers if needed - if (drawData->TotalVtxCount > vertexCount) { - // Clean up old buffer - RAII will handle this automatically - vertexBuffer = nullptr; - vertexBufferMemory = nullptr; + // Resize buffers if needed for this frame + if (frameIndex >= vertexCounts.size()) return; // Safety + + if (drawData->TotalVtxCount > vertexCounts[frameIndex]) { + // Clean up old buffer + vertexBuffers[frameIndex] = vk::raii::Buffer(nullptr); + vertexBufferMemories[frameIndex] = vk::raii::DeviceMemory(nullptr); // Create new vertex buffer vk::BufferCreateInfo bufferInfo; @@ -931,24 +950,24 @@ void ImGuiSystem::updateBuffers() { bufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer; bufferInfo.sharingMode = vk::SharingMode::eExclusive; - vertexBuffer = vk::raii::Buffer(device, bufferInfo); + vertexBuffers[frameIndex] = vk::raii::Buffer(device, bufferInfo); - vk::MemoryRequirements memRequirements = vertexBuffer.getMemoryRequirements(); + vk::MemoryRequirements memRequirements = vertexBuffers[frameIndex].getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo; allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); - vertexBufferMemory = vk::raii::DeviceMemory(device, allocInfo); - vertexBuffer.bindMemory(*vertexBufferMemory, 0); - vertexCount = drawData->TotalVtxCount; + vertexBufferMemories[frameIndex] = vk::raii::DeviceMemory(device, allocInfo); + vertexBuffers[frameIndex].bindMemory(*vertexBufferMemories[frameIndex], 0); + vertexCounts[frameIndex] = drawData->TotalVtxCount; } - if (drawData->TotalIdxCount > indexCount) { - // Clean up old buffer - RAII will handle this automatically - indexBuffer = nullptr; - indexBufferMemory = nullptr; + if (drawData->TotalIdxCount > indexCounts[frameIndex]) { + // Clean up old buffer + indexBuffers[frameIndex] = vk::raii::Buffer(nullptr); + indexBufferMemories[frameIndex] = vk::raii::DeviceMemory(nullptr); // Create new index buffer vk::BufferCreateInfo bufferInfo; @@ -956,23 +975,23 @@ void ImGuiSystem::updateBuffers() { bufferInfo.usage = vk::BufferUsageFlagBits::eIndexBuffer; bufferInfo.sharingMode = vk::SharingMode::eExclusive; - indexBuffer = vk::raii::Buffer(device, bufferInfo); + indexBuffers[frameIndex] = vk::raii::Buffer(device, bufferInfo); - vk::MemoryRequirements memRequirements = indexBuffer.getMemoryRequirements(); + vk::MemoryRequirements memRequirements = indexBuffers[frameIndex].getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo; allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); - indexBufferMemory = vk::raii::DeviceMemory(device, allocInfo); - indexBuffer.bindMemory(*indexBufferMemory, 0); - indexCount = drawData->TotalIdxCount; + indexBufferMemories[frameIndex] = vk::raii::DeviceMemory(device, allocInfo); + indexBuffers[frameIndex].bindMemory(*indexBufferMemories[frameIndex], 0); + indexCounts[frameIndex] = drawData->TotalIdxCount; } - // Upload data to buffers - void* vtxMappedMemory = vertexBufferMemory.mapMemory(0, vertexBufferSize); - void* idxMappedMemory = indexBufferMemory.mapMemory(0, indexBufferSize); + // Upload data to buffers for this frame + void* vtxMappedMemory = vertexBufferMemories[frameIndex].mapMemory(0, vertexBufferSize); + void* idxMappedMemory = indexBufferMemories[frameIndex].mapMemory(0, indexBufferSize); ImDrawVert* vtxDst = static_cast(vtxMappedMemory); ImDrawIdx* idxDst = static_cast(idxMappedMemory); @@ -985,8 +1004,8 @@ void ImGuiSystem::updateBuffers() { idxDst += cmdList->IdxBuffer.Size; } - vertexBufferMemory.unmapMemory(); - indexBufferMemory.unmapMemory(); + vertexBufferMemories[frameIndex].unmapMemory(); + indexBufferMemories[frameIndex].unmapMemory(); } catch (const std::exception& e) { std::cerr << "Failed to update buffers: " << e.what() << std::endl; } diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index 3dba6830..f9e2c237 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -53,7 +53,7 @@ class ImGuiSystem { * @brief Render the ImGui frame. * @param commandBuffer The command buffer to record rendering commands to. */ - void Render(vk::raii::CommandBuffer & commandBuffer); + void Render(vk::raii::CommandBuffer & commandBuffer, uint32_t frameIndex); /** * @brief Handle mouse input. @@ -146,12 +146,13 @@ class ImGuiSystem { vk::raii::Image fontImage = nullptr; vk::raii::DeviceMemory fontMemory = nullptr; vk::raii::ImageView fontView = nullptr; - vk::raii::Buffer vertexBuffer = nullptr; - vk::raii::DeviceMemory vertexBufferMemory = nullptr; - vk::raii::Buffer indexBuffer = nullptr; - vk::raii::DeviceMemory indexBufferMemory = nullptr; - uint32_t vertexCount = 0; - uint32_t indexCount = 0; + // Per-frame dynamic buffers to avoid GPU/CPU contention when frames are in flight + std::vector vertexBuffers; + std::vector vertexBufferMemories; + std::vector indexBuffers; + std::vector indexBufferMemories; + std::vector vertexCounts; + std::vector indexCounts; // Window dimensions uint32_t width = 0; @@ -217,5 +218,5 @@ class ImGuiSystem { /** * @brief Update vertex and index buffers. */ - void updateBuffers(); + void updateBuffers(uint32_t frameIndex); }; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 7d7e2b4f..a26242e8 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -180,6 +180,9 @@ class Renderer { */ vk::Device GetDevice() const { return *device; } + // Expose max frames in flight for per-frame resource duplication + uint32_t GetMaxFramesInFlight() const { return MAX_FRAMES_IN_FLIGHT; } + /** * @brief Get the Vulkan RAII device. * @return The Vulkan RAII device. diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 99fda207..5a459be6 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -767,7 +767,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Render ImGui if provided if (imguiSystem) { - imguiSystem->Render(commandBuffers[currentFrame]); + imguiSystem->Render(commandBuffers[currentFrame], currentFrame); } // End dynamic rendering From 60ee95d374476dd7c5e7acb91e40d3b428d77c11 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:02:52 -0700 Subject: [PATCH 069/102] addressing more comments. --- .../GUI/02_imgui_setup.adoc | 7 ++- .../GUI/04_ui_elements.adoc | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc index 888195e1..3a3a7131 100644 --- a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc +++ b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc @@ -111,6 +111,11 @@ Next, we set up the Vulkan pipeline objects that define how UI geometry is proce The pipeline infrastructure creates a specialized graphics pipeline optimized for UI rendering, which differs significantly from typical 3D rendering pipelines. UI rendering typically requires alpha blending for transparency effects, operates in 2D screen space rather than 3D world space, and uses simpler shading models focused on texture sampling rather than complex lighting calculations. +[NOTE] +==== +Frames-in-flight safety: If your renderer uses more than one frame in flight and you do not stall the GPU between frames, you must duplicate the dynamic ImGui buffers (vertex/index) per frame-in-flight. Using a single shared vertex/index buffer risks the CPU overwriting data still in use by the GPU from a previous frame. The simple single-buffer members shown above are for conceptual clarity; in production, store vectors of buffers/memories sized to the max frames in flight and update/bind the buffers for the current frame index. +==== + The descriptor system manages the connection between our CPU-side resources and the GPU shaders. For UI rendering, this primarily involves binding the font atlas texture to the fragment shader, though more complex UI systems might include additional textures for icons, backgrounds, or other visual elements. === ImGuiVulkanUtil Architecture: Device Context and System Integration @@ -773,7 +778,7 @@ void drawFrame() { // Render scene using dynamic rendering // ... - // Render ImGui + // Render ImGui (in multi-frame renderers, pass the current frame index to bind per-frame buffers) imGui.drawFrame(commandBuffer); // ... submit command buffer ... diff --git a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc index 3b619480..ad4b9688 100644 --- a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc +++ b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc @@ -111,6 +111,69 @@ When integrating ImGui with Vulkan, consider these performance aspects: 4. *Pipeline State*: Use a dedicated pipeline for ImGui to minimize state changes 5. *Render Pass Integration*: Consider whether to use a separate render pass or subpass for the GUI +==== Frames-in-Flight: Duplicate Dynamic Buffers Per Frame + +If your renderer uses multiple frames in flight (e.g., double/triple buffering) without a device wait-idle between frames, ImGui's dynamic vertex and index buffers must not be shared across frames. Otherwise, the CPU can overwrite data that the GPU from a previous frame is still reading. + +- Allocate one vertex buffer and one index buffer per frame-in-flight. +- Update/bind the buffers for the current frame index only. +- Size each buffer to the frame's ImDrawData TotalVtxCount/TotalIdxCount, growing as needed. + +Example sketch: + +[source,cpp] +---- +class ImGuiSystem { + // ... + std::vector vertexBuffers; + std::vector vertexMemories; + std::vector indexBuffers; + std::vector indexMemories; + std::vector vertexCounts; + std::vector indexCounts; + + bool Initialize(Renderer* renderer, uint32_t w, uint32_t h) { + // ... create pipelines, font, descriptors ... + const uint32_t frames = renderer->GetMaxFramesInFlight(); + vertexBuffers.resize(frames); + vertexMemories.resize(frames); + indexBuffers.resize(frames); + indexMemories.resize(frames); + vertexCounts.assign(frames, 0); + indexCounts.assign(frames, 0); + return true; + } + + void Render(vk::raii::CommandBuffer& cmd, uint32_t frameIndex) { + ImGui::Render(); + updateBuffers(frameIndex); + // bind per-frame buffers + std::array vb = {*vertexBuffers[frameIndex]}; + std::array offs{}; + cmd.bindVertexBuffers(0, vb, offs); + cmd.bindIndexBuffer(*indexBuffers[frameIndex], 0, vk::IndexType::eUint16); + // draw lists... + } + + void updateBuffers(uint32_t frameIndex) { + ImDrawData* dd = ImGui::GetDrawData(); + if (!dd || dd->CmdListsCount == 0) return; + vk::DeviceSize vbytes = dd->TotalVtxCount * sizeof(ImDrawVert); + vk::DeviceSize ibytes = dd->TotalIdxCount * sizeof(ImDrawIdx); + // grow-per-frame if needed, then map/copy for this frame only + // ... + } +}; +---- + +When integrating with your main renderer, pass the current frame index to the ImGui render call: + +[source,cpp] +---- +// inside your frame loop after scene rendering +imguiSystem->Render(commandBuffers[currentFrame], currentFrame); +---- + === Organizing Your GUI Code For maintainable GUI code, consider these organizational patterns: From 23952bc07f83afa918d1d2511d1a5beea6098d0e Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:08:29 -0700 Subject: [PATCH 070/102] addressing more comments. --- .../GUI/05_vulkan_integration.adoc | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc index 8bf28eca..6260f4b1 100644 --- a/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc @@ -263,6 +263,26 @@ The UI rendering phase begins with its own command buffer recording, configured // This is the key difference from scene rendering - we load existing content colorAttachment.loadOp = vk::AttachmentLoadOp::eLoad; // Preserve scene rendering + // Ensure proper ordering/visibility between scene and UI when using multiple command buffers. + // If you submit scene and UI command buffers separately, synchronize them either by: + // - Submitting both on the same queue with a semaphore (scene signals, UI waits with stage = COLOR_ATTACHMENT_OUTPUT), or + // - Recording a pipeline barrier in the UI command buffer before beginRendering() to make scene color writes visible. + // Example barrier inserted in the UI command buffer: + { + vk::ImageMemoryBarrier2 barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentRead | vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .image = *swapChainImages[imageIndex], + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + vk::DependencyInfo depInfo{ .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier }; + imguiCommandBuffer.pipelineBarrier2(depInfo); + } + // UI rendering typically doesn't need depth testing // Remove depth attachment to optimize UI rendering performance renderingInfo.pDepthAttachment = nullptr; @@ -372,24 +392,31 @@ public: } void createImGuiDescriptorPool() { + // ImGui typically needs a handful of descriptors (font texture + user UI textures). + // Adjust these values to your app's needs (e.g., expected number of UI textures, buffers). + // As a starting point: vk::DescriptorPoolSize poolSizes[] = { - { vk::DescriptorType::eSampler, 1000 }, - { vk::DescriptorType::eCombinedImageSampler, 1000 }, - { vk::DescriptorType::eSampledImage, 1000 }, - { vk::DescriptorType::eStorageImage, 1000 }, - { vk::DescriptorType::eUniformTexelBuffer, 1000 }, - { vk::DescriptorType::eStorageTexelBuffer, 1000 }, - { vk::DescriptorType::eUniformBuffer, 1000 }, - { vk::DescriptorType::eStorageBuffer, 1000 }, - { vk::DescriptorType::eUniformBufferDynamic, 1000 }, - { vk::DescriptorType::eStorageBufferDynamic, 1000 }, - { vk::DescriptorType::eInputAttachment, 1000 } + { vk::DescriptorType::eSampler, 8 }, + { vk::DescriptorType::eCombinedImageSampler, 128 }, // font + user-provided textures + { vk::DescriptorType::eSampledImage, 128 }, + { vk::DescriptorType::eStorageImage, 8 }, + { vk::DescriptorType::eUniformTexelBuffer, 8 }, + { vk::DescriptorType::eStorageTexelBuffer, 8 }, + { vk::DescriptorType::eUniformBuffer, 32 }, + { vk::DescriptorType::eStorageBuffer, 32 }, + { vk::DescriptorType::eUniformBufferDynamic, 16 }, + { vk::DescriptorType::eStorageBufferDynamic, 16 }, + { vk::DescriptorType::eInputAttachment, 8 } }; + // A conservative maxSets equals the sum of descriptor counts. + uint32_t maxSets = 0; + for (const auto& ps : poolSizes) maxSets += ps.descriptorCount; + vk::DescriptorPoolCreateInfo poolInfo{ .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .maxSets = 1000, + .maxSets = maxSets, .poolSizeCount = static_cast(std::size(poolSizes)), .pPoolSizes = poolSizes }; @@ -890,24 +917,28 @@ void ImGuiUtil::SetInputCallback(std::function callback) { } void ImGuiUtil::createDescriptorPool() { + // Tune these to match your expected number of UI textures and buffers. vk::DescriptorPoolSize poolSizes[] = { - { vk::DescriptorType::eSampler, 1000 }, - { vk::DescriptorType::eCombinedImageSampler, 1000 }, - { vk::DescriptorType::eSampledImage, 1000 }, - { vk::DescriptorType::eStorageImage, 1000 }, - { vk::DescriptorType::eUniformTexelBuffer, 1000 }, - { vk::DescriptorType::eStorageTexelBuffer, 1000 }, - { vk::DescriptorType::eUniformBuffer, 1000 }, - { vk::DescriptorType::eStorageBuffer, 1000 }, - { vk::DescriptorType::eUniformBufferDynamic, 1000 }, - { vk::DescriptorType::eStorageBufferDynamic, 1000 }, - { vk::DescriptorType::eInputAttachment, 1000 } + { vk::DescriptorType::eSampler, 8 }, + { vk::DescriptorType::eCombinedImageSampler, 128 }, + { vk::DescriptorType::eSampledImage, 128 }, + { vk::DescriptorType::eStorageImage, 8 }, + { vk::DescriptorType::eUniformTexelBuffer, 8 }, + { vk::DescriptorType::eStorageTexelBuffer, 8 }, + { vk::DescriptorType::eUniformBuffer, 32 }, + { vk::DescriptorType::eStorageBuffer, 32 }, + { vk::DescriptorType::eUniformBufferDynamic, 16 }, + { vk::DescriptorType::eStorageBufferDynamic, 16 }, + { vk::DescriptorType::eInputAttachment, 8 } }; + uint32_t maxSets = 0; + for (const auto& ps : poolSizes) maxSets += ps.descriptorCount; + vk::DescriptorPoolCreateInfo poolInfo{ .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .maxSets = 1000, + .maxSets = maxSets, .poolSizeCount = static_cast(std::size(poolSizes)), .pPoolSizes = poolSizes }; From 65e5db3608225e550eff385a569f2eb01c2c8c0b Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:09:43 -0700 Subject: [PATCH 071/102] addressing more comments. --- en/Building_a_Simple_Engine/GUI/06_conclusion.adoc | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc index 798b1d0c..b1929d37 100644 --- a/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc @@ -90,18 +90,6 @@ While we've focused on https://github.com/ocornut/imgui[Dear ImGui] in this chap * https://www.noesisengine.com/[*Noesis GUI*]: A commercial UI middleware that supports XAML and can render through Vulkan. It's used in games like Dauntless and provides a designer-friendly workflow. -Popular game engines have different approaches to GUI integration with Vulkan: - -* https://www.unrealengine.com/[*Unreal Engine*]: Uses its own proprietary Slate UI framework internally, which has been adapted to work with Vulkan when the engine uses this rendering backend. For in-game UIs, it uses https://docs.unrealengine.com/5.0/en-US/umg-ui-designer-for-unreal-engine/[Unreal Motion Graphics (UMG)], which is built on top of Slate. - -* https://unity.com/[*Unity*]: When using the Vulkan renderer, Unity's built-in UI system (https://docs.unity3d.com/Manual/UIToolkit.html[UI Toolkit] and legacy https://docs.unity3d.com/Manual/com.unity.ugui.html[UGUI]) works transparently with Vulkan. Unity abstracts the rendering details away from the UI system. - -* https://godotengine.org/[*Godot*]: Uses its own built-in UI system that works across all its rendering backends, including Vulkan. The UI nodes are integrated directly into the scene tree. - -* https://www.cryengine.com/[*CryEngine*]: Employs https://www.autodesk.com/products/scaleform/overview[Scaleform] for UI rendering, which has been adapted to work with the engine's Vulkan implementation. - -* *Custom engines*: Many AAA studios with proprietary engines either develop custom UI solutions or integrate middleware like https://coherent-labs.com/[Coherent UI] or https://www.autodesk.com/products/scaleform/overview[Scaleform], adapting them to work with their Vulkan renderers. - When choosing a GUI library for your Vulkan application, consider factors like: * Development paradigm (immediate-mode vs. retained-mode) From e564b29cc366ce83819a93d5c751de1e1d8799d6 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:14:46 -0700 Subject: [PATCH 072/102] addressing more comments. --- .../Engine_Architecture/01_introduction.adoc | 8 ++++++++ en/Building_a_Simple_Engine/GUI/01_introduction.adoc | 9 +++++++++ .../Mobile_Development/01_introduction.adoc | 9 +++++++++ .../Subsystems/01_introduction.adoc | 9 +++++++++ en/Building_a_Simple_Engine/Tooling/01_introduction.adoc | 8 ++++++++ 5 files changed, 43 insertions(+) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc index a3841ce0..2de95723 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -37,6 +37,14 @@ Beyond Vulkan knowledge, you'll benefit from familiarity with object-oriented pr Modern C++ features play a crucial role in our implementation approach. Smart pointers help us manage resource lifetimes safely, templates enable flexible, reusable components, and other C++11/14/17 features allow us to write more expressive and maintainable code. If you're not comfortable with these concepts, consider reviewing them before proceeding. +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] + === Why Architecture Matters The difference between a hastily assembled renderer and a well-architected engine becomes apparent as soon as you need to make changes or add features. Good architecture creates a foundation that supports your development process rather than fighting against it. diff --git a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc index 420f9ee6..b3b2b67a 100644 --- a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc @@ -32,6 +32,15 @@ Pipeline creation knowledge ties everything together, as we'll build a specializ A basic understanding of input handling concepts will help you follow along as we implement the dual-mode input system that can distinguish between 3D navigation and GUI interaction. +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* xref:../../06_Texture_mapping/00_Images.adoc[Texture mapping] + Let's begin by exploring how to implement a professional GUI system with Dear ImGui and Vulkan. link:../Lighting_Materials/06_conclusion.adoc[Previous: Lighting & Materials Conclusion] | link:02_imgui_setup.adoc[Next: Setting Up Dear ImGui] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc index c2e8e1f3..066fd102 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc @@ -26,6 +26,15 @@ Modern C++ expertise becomes particularly valuable in mobile development, where Understanding basic mobile development concepts will provide crucial context for the platform-specific decisions we'll make. Mobile applications operate under constraints that desktop applications rarely face—app lifecycle events, varying screen densities, touch input paradigms, and the need to preserve battery life all influence how we design and implement our Vulkan engine for mobile platforms. +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* xref:../../11_Compute_Shader.adoc[Compute shaders] + Let's begin by exploring the platform considerations for Android and iOS. link:../Tooling/07_conclusion.adoc[Previous: Tooling Conclusion] | link:02_platform_considerations.adoc[Next: Platform Considerations for Android and iOS] diff --git a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc index 6b34cc9e..00e041aa 100644 --- a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc @@ -42,6 +42,15 @@ Experience with Vulkan compute shaders is essential, as we'll leverage compute c A basic understanding of audio and physics concepts in game development will help you appreciate the design decisions we make throughout the implementation. While we'll explain the fundamentals as we build each system, familiarity with concepts like sound attenuation, collision detection, and rigid body dynamics will deepen your understanding of how these subsystems serve the broader goals of interactive applications. +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* xref:../../11_Compute_Shader.adoc[Compute shaders] + Let's begin by exploring how to implement a basic audio subsystem and then enhance it with Vulkan's computational capabilities. link:../Loading_Models/09_conclusion.adoc[Previous: Loading Models Conclusion] | link:02_audio_basics.adoc[Next: Audio Basics] diff --git a/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc index 7b2ddfb1..4576d626 100644 --- a/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc @@ -26,6 +26,14 @@ Experience with modern C++ concepts becomes particularly important here, as prof A basic familiarity with software development workflows and tools will provide context for the systems we'll build. While we'll explain the specific implementations, understanding why continuous integration matters, how debugging fits into development cycles, and why crash reporting improves user experience will help you appreciate the architectural decisions we make throughout this chapter. +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] + Let's begin by exploring how to set up a CI/CD pipeline for Vulkan projects. link:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:02_cicd.adoc[Next: CI/CD for Vulkan Projects] From 7b3e016e00ec2c0554e2604efdad5c2c79d4810c Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:18:40 -0700 Subject: [PATCH 073/102] addressing more comments. --- .../Subsystems/01_introduction.adoc | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc index 00e041aa..28a0fbff 100644 --- a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc @@ -34,6 +34,26 @@ for more than just Graphics in your application. Our goal with this tutorial with the tools necessary to tackle any Vulkan application development and to think critically about how your applications can benefit from the GPU. +=== Practical considerations: Don't offload everything to the GPU + +While Vulkan compute can deliver impressive speedups, it's not always advisable to offload every subsystem to the GPU—especially on mobile: + +* Mobile power and thermals: Many phones and tablets use big.LITTLE CPU clusters and mobile GPUs that are power/thermal constrained. Sustained heavy GPU compute can quickly lead to thermal throttling, causing frame rate drops and inconsistent latency. +* Scheduling and latency: GPUs excel at throughput, but certain tasks (tight control loops, small pointer-heavy updates) can prefer CPU execution due to launch overheads and scheduling latency. +* Determinism and debugging: For gameplay-critical physics, determinism and step-by-step debugging on the CPU can be advantageous. Consider keeping broad-phase or whole-physics on the CPU on mobile, or use a hybrid approach (e.g., CPU broad-phase + GPU narrow-phase). +* Memory bandwidth: On integrated architectures, GPU/CPU share memory bandwidth. Aggressively moving everything to GPU can contend with graphics and hurt frame time and battery life. + +Audio-specific guidance: + +* Prefer platform audio APIs and any available dedicated audio hardware/DSP when feasible (for mixing, resampling, effects). This path often provides lower latency, better power characteristics, and more predictable behavior across devices. +* HRTF support in dedicated hardware is not widespread in the wild. Many consumer devices rely on software HRTF in the OS or application. Evaluate your needs: a software HRTF pipeline may be perfectly adequate; reserving GPU compute strictly for spatial audio is rarely necessary unless you have many sources or complex effects. + +Practical recommendations: + +* Profile first: Establish CPU and GPU baselines before moving work. +* Favor hybrid designs: Offload the clearly parallel, heavy kernels (e.g., batched constraint solves, FFT/IR convolution) while keeping control/coordination on CPU. +* Plan for mobility: Provide runtime toggles to switch between CPU/GPU paths based on device class, thermal state, and power mode. + === Prerequisites This chapter builds extensively on the engine architecture and Vulkan foundations established in previous chapters. The modular design patterns we've implemented become crucial when adding subsystems that need to integrate cleanly with existing rendering, camera, and resource management systems. From 7bf9dab8db0f47945ac571d582be0ec2655b534f Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:22:39 -0700 Subject: [PATCH 074/102] addressing more comments. --- .../Subsystems/03_vulkan_audio.adoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc index d7eec7bf..55b94585 100644 --- a/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc @@ -20,6 +20,7 @@ The challenge with HRTF processing is that it's computationally expensive: This is where Vulkan compute shaders can help by offloading these calculations to the GPU. +[[audio-vulkan-why]] === Why Use Vulkan for Audio Processing? Traditional audio processing is done on the CPU, but there are several advantages to using Vulkan compute shaders for certain audio tasks: @@ -404,12 +405,10 @@ void AudioSystem::Shutdown() { === Advantages of Vulkan-Based HRTF -By implementing HRTF processing with Vulkan compute shaders, we gain several advantages: +See the core benefits listed in <> for a summary of why compute shaders are a good fit. In the context of HRTF specifically, two practical advantages are worth highlighting: -1. *Scalability*: The GPU can process hundreds or thousands of sound sources in parallel. -2. *Quality*: We can use higher-order HRTF filters without significant performance impact. -3. *CPU Offloading*: Audio processing no longer competes with game logic for CPU resources. -4. *Advanced Effects*: The GPU's computational power enables more complex audio effects like room acoustics simulation. +1. *Quality*: You can afford higher-order HRTF filters without significant performance impact, improving spatial realism. +2. *Advanced Effects*: The GPU's compute power enables more sophisticated effects (e.g., room acoustics simulation) alongside HRTF. === Limitations and Considerations From 47a6baae3345b0ff89d87811edad7bffbb50d0a2 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:31:22 -0700 Subject: [PATCH 075/102] addressing more comments. --- .../Subsystems/06_conclusion.adoc | 4 ++-- en/Building_a_Simple_Engine/Tooling/02_cicd.adoc | 8 ++++---- .../Tooling/03_debugging_and_renderdoc.adoc | 12 +++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc index f83f88ca..c130b31c 100644 --- a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc @@ -67,9 +67,9 @@ While our implementations provide a solid foundation, there are several areas wh ==== General Improvements -* *Cross-Platform Support*: Ensure the subsystems work across different platforms, with appropriate fallbacks for devices without Vulkan support. +* *Driver and Platform Coverage*: Test the subsystems across a representative set of Vulkan-capable platforms and drivers (e.g., Windows/Linux, major IHVs, Android, and macOS via MoltenVK). Non-Vulkan fallbacks are out of scope for this tutorial. * *Profiling and Optimization*: Add detailed profiling to identify and address performance bottlenecks. -* *Memory Management*: Implement more sophisticated memory management to reduce fragmentation and improve cache coherency. +* *Memory Management*: Use allocator suballocation strategies (e.g., Vulkan Memory Allocator or custom pools), batch buffer/image allocations, and group resources by usage to reduce fragmentation and improve cache locality. * *Multi-Threading*: Further optimize CPU-side processing with multi-threading where appropriate. === Integration with Other Engine Systems diff --git a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc index 45f0625a..aac3d0b5 100644 --- a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc +++ b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc @@ -80,11 +80,11 @@ jobs: When setting up CI/CD for Vulkan projects, consider these specific challenges: -===== 1. Vulkan SDK Installation +===== Vulkan SDK Installation Ensure your CI environment has the Vulkan SDK installed. Many CI platforms don't include it by default. In the example above, we used a GitHub Action to install the SDK. -===== 2. GPU Availability in CI Environments +===== GPU Availability in CI Environments Most CI environments don't have GPUs available, which can make testing Vulkan applications challenging. Consider these approaches: @@ -92,7 +92,7 @@ Most CI environments don't have GPUs available, which can make testing Vulkan ap * Implement a headless testing mode that doesn't require a display * Use cloud-based GPU instances for more comprehensive testing -===== 3. Platform-Specific Vulkan Loaders +===== Platform-Specific Vulkan Loaders Different platforms handle Vulkan loading differently. Ensure your build system correctly handles these differences: @@ -100,7 +100,7 @@ Different platforms handle Vulkan loading differently. Ensure your build system * Linux: libvulkan.so.1 is loaded at runtime * macOS: MoltenVK provides Vulkan support via Metal -===== 4. Shader Compilation +===== Shader Compilation Shader compilation can be a complex part of the build process. Consider these approaches: diff --git a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc index f2324638..e5898e8c 100644 --- a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc +++ b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc @@ -191,7 +191,9 @@ void submit_work(vk::raii::Queue& queue, vk::raii::CommandBuffer& cmd_buffer) { === Using RenderDoc -RenderDoc is a powerful graphics debugging tool that allows you to capture frames from your application and analyze them in detail. It's particularly useful for Vulkan applications due to its comprehensive support for the API. +RenderDoc is a graphics frame debugger and capture/analysis tool (not a compiler). It allows you to capture frames from your application and analyze them in detail. It's particularly useful for Vulkan applications due to its comprehensive support for the API. + +NOTE: In this section we refer to the Event Browser, Pipeline State, and Resource Inspector panels in RenderDoc. Screenshots should accompany these instructions to show where debug names and labels appear in the UI. ==== Integrating RenderDoc with Your Application @@ -261,10 +263,10 @@ Once you've captured a frame, you can analyze it in the RenderDoc application. H To get the most out of RenderDoc: -1. *Use Object Names*: As discussed earlier, naming your Vulkan objects makes them much easier to identify in RenderDoc -2. *Use Command Buffer Labels*: These appear in RenderDoc's event browser, making it easier to navigate complex frames -3. *Capture Specific Frames*: Use the in-application API to capture frames where issues occur -4. *Reduce Workload*: For complex applications, consider disabling features or reducing resolution when debugging +1. *Use Object Names*: As discussed earlier, naming your Vulkan objects makes them much easier to identify in RenderDoc (you'll see them in the Resource Inspector and Pipeline State views). +2. *Use Command Buffer Labels*: These appear in RenderDoc's Event Browser and help you navigate to the relevant draw/dispatch quickly. +3. *Capture the Problem Frame*: Trigger a capture exactly when the issue occurs (via hotkey or the in-application API) to minimize unrelated events and noise. +4. *Minimize to a Repro*: Create a minimal reproducible scene or toggle features off to isolate the problem. If you reduce resolution, make sure it doesn't alter ordering/timing in a way that hides the bug. === Combining VK_KHR_debug_utils and RenderDoc From 44e7f38e6c177cedb6b21de57da048e141c4804b Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:32:39 -0700 Subject: [PATCH 076/102] addressing more comments. --- .../Tooling/03_debugging_and_renderdoc.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc index e5898e8c..8b7820d9 100644 --- a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc +++ b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc @@ -193,7 +193,6 @@ void submit_work(vk::raii::Queue& queue, vk::raii::CommandBuffer& cmd_buffer) { RenderDoc is a graphics frame debugger and capture/analysis tool (not a compiler). It allows you to capture frames from your application and analyze them in detail. It's particularly useful for Vulkan applications due to its comprehensive support for the API. -NOTE: In this section we refer to the Event Browser, Pipeline State, and Resource Inspector panels in RenderDoc. Screenshots should accompany these instructions to show where debug names and labels appear in the UI. ==== Integrating RenderDoc with Your Application From dd51a38378c6f7e8dcb89498e29ccd4b625631c9 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:38:57 -0700 Subject: [PATCH 077/102] addressing more comments. --- .../Tooling/04_crash_minidump.adoc | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc index 165159a2..84dde25b 100644 --- a/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc +++ b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc @@ -1,10 +1,10 @@ :pp: {plus}{plus} -= Tooling: Crash Handling and Minidumps += Tooling: Crash Handling and GPU Crash Dumps == Crash Handling in Vulkan Applications -Even with thorough testing and debugging, crashes can still occur in production environments. When they do, having robust crash handling mechanisms can help you diagnose and fix issues quickly. In this section, we'll explore how to implement crash handling and generate minidumps in Vulkan applications. +Even with thorough testing and debugging, crashes can still occur in production environments. When they do, having robust crash handling mechanisms can help you diagnose and fix issues quickly. This chapter focuses on practical GPU crash diagnostics (e.g., NVIDIA Nsight Aftermath, AMD Radeon GPU Detective) and clarifies the role and limitations of OS process minidumps, which usually lack GPU state and are rarely sufficient to root-cause graphics/device-lost issues on their own. === Understanding Crashes in Vulkan Applications @@ -178,11 +178,49 @@ int main() { } ---- +=== GPU Crash Diagnostics (Vulkan) + +While OS process minidumps capture CPU-side state, GPU crashes (device lost, TDRs, hangs) require GPU-specific crash dumps to be actionable. In practice, you’ll want to integrate vendor tooling that can record GPU execution state around the fault. + +==== NVIDIA: Nsight Aftermath (Vulkan) + +Overview: + +- Collects GPU crash dumps with information about the last executed draw/dispatch, bound pipeline/shaders, markers, and resource identifiers. +- Works alongside your Vulkan app; you analyze dumps with NVIDIA tools to pinpoint the failing work and shader. + +Practical steps: + +1. Enable object names and labels + - Use VK_EXT_debug_utils to name pipelines, shaders, images, buffers, and to insert command buffer labels for major passes and draw/dispatch groups. These names surface in crash reports and greatly aid triage. +2. Add frame/work markers + - Insert meaningful labels before/after critical rendering phases. If available on your target, also use vendor checkpoint/marker extensions (e.g., VK_NV_device_diagnostic_checkpoints) to provide fine-grained breadcrumbs. +3. Build shaders with unique IDs and optional debug info + - Ensure each pipeline/shader can be correlated (e.g., include a stable hash/UUID in your pipeline cache and application logs). Keep the mapping from IDs to source for analysis. +4. Initialize and enable GPU crash dumps + - Integrate the Nsight Aftermath Vulkan SDK per NVIDIA’s documentation. Register a callback to receive crash dump data, write it to disk, and include your marker string table for symbolication. +5. Handle device loss + - On VK_ERROR_DEVICE_LOST (or Windows TDR), flush any in-memory marker logs, persist the crash dump, and then terminate cleanly. Attempting to continue rendering is undefined. + +References: NVIDIA Nsight Aftermath SDK and documentation. + +==== AMD: Radeon GPU Detective (RGD) + +- AMD provides tools to capture and analyze GPU crash information on RDNA hardware. Similar principles apply: enable object names, label command buffers, and preserve pipeline/shader identifiers so RGD can point back to your content. +- See AMD Radeon GPU Detective and related documentation for Vulkan integration and analysis workflows. + +==== Vendor-agnostic groundwork that helps all tools + +- Name everything via VK_EXT_debug_utils. +- Insert command buffer labels at meaningful boundaries (frame, pass, material batch, etc.). +- Persist build/version, driver, Vulkan API/UUID, and pipeline cache UUID in your logs and crash artifacts. +- Implement robust device lost handling: stop submitting, free/teardown safely, write artifacts, exit. + === Generating Minidumps -While basic crash logs are helpful, minidumps provide much more detailed information for diagnosing crashes. A minidump is a file containing a snapshot of the process memory and state at the time of the crash. +Use OS process minidumps to capture CPU-side call stacks, threads, and memory snapshots at the time of a crash. For graphics issues and device loss, they rarely contain the GPU execution state you need—treat minidumps as a complement to GPU crash dumps, not a replacement. -Let's implement minidump generation using platform-specific APIs: +Below is a brief outline for generating minidumps with platform APIs (useful for correlating CPU context with a GPU crash): [source,cpp] ---- @@ -303,7 +341,7 @@ namespace crash_handler { === Analyzing Minidumps -Once you have a minidump, you need to analyze it to determine the cause of the crash. Here's how to do this on different platforms: +Minidumps are best used to understand CPU-side state around a crash (e.g., which thread faulted, call stacks leading to vkQueueSubmit/vkQueuePresent, allocator misuse) and to correlate with a GPU crash dump from vendor tools. Here’s a brief workflow on different platforms: ==== Windows @@ -467,26 +505,29 @@ namespace crash_handler { } ---- -=== Best Practices for Crash Handling - -To make the most of your crash handling system: - -1. *Always Include Version Information*: Make sure your crash reports include the application version, Vulkan version, and driver version. - -2. *Collect Relevant State*: Include information about what the application was doing when it crashed (e.g., loading a model, rendering a specific scene). - -3. *Respect User Privacy*: Be transparent about what data you collect and get user consent before uploading crash reports. - -4. *Test Your Crash Handling*: Deliberately trigger crashes in different scenarios to ensure your crash handling system works correctly. - -5. *Implement Graceful Recovery*: When possible, try to recover from non-fatal errors rather than crashing. - -6. *Use Crash Reports to Improve*: Regularly analyze crash reports to identify and fix common issues. +=== Best Practices for Crash Handling (Vulkan/GPU-focused) + +To make crash data actionable for graphics issues, prefer these concrete steps: + +1. Name and label aggressively + - Use VK_EXT_debug_utils to name all objects and insert command buffer labels at pass/material boundaries and before large draw/dispatch batches. Persist a small in-memory ring buffer of recent labels for inclusion in crash artifacts. +2. Prepare for device loss + - Implement a central handler for VK_ERROR_DEVICE_LOST. Stop submitting work, flush logs/markers, request vendor GPU crash dump data, and exit. Avoid attempting recovery in the same process unless you have a robust reinitialization path. +3. Capture GPU crash dumps on supported hardware + - Integrate NVIDIA Nsight Aftermath and/or AMD RGD depending on your target audience. Ship with crash dumps enabled in development/beta builds; provide a toggle for users. +4. Make builds symbol-friendly + - Keep a mapping from pipeline/shader hashes to source/IR/SPIR-V and build IDs. Enable shader debug info where feasible for diagnosis builds. +5. Record environment info + - Log driver version, Vulkan version, GPU name/PCI ID, pipeline cache UUID, app build/version, and relevant feature toggles. Include this alongside minidumps and GPU crash dumps. +6. Reproduce deterministically + - Provide a way to disable background variability (e.g., async streaming) and to replay a captured sequence of commands/scenes to reproduce the crash locally. +7. Respect privacy and distribution concerns + - Clearly document what crash data is collected (minidumps, GPU crash dumps, logs) and require opt‑in for uploads. Strip user-identifiable data. === Conclusion -Robust crash handling is essential for maintaining a high-quality Vulkan application. By implementing proper crash handling and minidump generation, you can quickly diagnose and fix issues that occur in production environments, leading to a more stable and reliable application. +Robust crash handling is essential for maintaining a high-quality Vulkan application. Combine vendor GPU crash dumps (Aftermath, RGD, etc.) with CPU-side minidumps and thorough logging to quickly diagnose and fix issues in production. Treat minidumps as complementary context; the actionable details for graphics faults typically come from GPU crash dump tooling. -In the next section, we'll explore Vulkan extensions for robustness, which can help prevent crashes in the first place by making your application more resilient to undefined behavior. +In the next section, we'll explore Vulkan extensions for robustness, which can reduce undefined behavior and help prevent crashes in the first place. link:03_debugging_and_renderdoc.adoc[Previous: Debugging with VK_KHR_debug_utils and RenderDoc] | link:05_extensions.adoc[Next: Vulkan Extensions for Robustness] From f713e9c9983c23e328e67513c818a6b661490c68 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:46:24 -0700 Subject: [PATCH 078/102] addressing more comments. --- .../Tooling/05_extensions.adoc | 80 +++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc index b7013eb7..47db7e90 100644 --- a/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc +++ b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc @@ -139,7 +139,7 @@ While VK_EXT_robustness2 is the focus of this section, there are other extension ==== VK_KHR_buffer_device_address -This extension allows you to use physical device addresses for buffers, which can be useful for advanced techniques. It includes robustness features for handling invalid addresses: +This extension allows you to use physical device addresses for buffers, which can be useful for advanced techniques. It includes robustness features for handling invalid addresses (when combined with robust access features like VK_EXT_robustness2 or core robustBufferAccess): [source,cpp] ---- @@ -148,19 +148,28 @@ void enable_buffer_device_address(vk::DeviceCreateInfo& device_create_info, enabled_extensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); device_create_info.setPEnabledExtensionNames(enabled_extensions); + // Enable Buffer Device Address features vk::PhysicalDeviceBufferDeviceAddressFeatures buffer_device_address_features{}; buffer_device_address_features.setBufferDeviceAddress(VK_TRUE); buffer_device_address_features.setBufferDeviceAddressCaptureReplay(VK_TRUE); - // Add to the pNext chain + // Optionally chain robustness features to ensure invalid addresses read as zero and writes are discarded + // (If you've already enabled VK_EXT_robustness2 elsewhere, this is not required here.) + vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features{}; + robustness2_features.setRobustBufferAccess2(VK_TRUE); + robustness2_features.setRobustImageAccess2(VK_TRUE); + robustness2_features.setNullDescriptor(VK_TRUE); + + // Chain features: robustness2 -> BDA -> existing pNext + robustness2_features.pNext = &buffer_device_address_features; buffer_device_address_features.pNext = device_create_info.pNext; - device_create_info.pNext = &buffer_device_address_features; + device_create_info.pNext = &robustness2_features; } ---- ==== VK_EXT_descriptor_indexing -This extension allows for more flexible descriptor indexing, including robustness features for handling out-of-bounds descriptor array accesses: +This extension allows for more flexible descriptor indexing, including robustness-related capabilities such as tolerating out-of-bounds indices (reads become zero when robust access is enabled), partially bound descriptor sets, and update-after-bind. To actually make use of these behaviors you need to enable both device features and descriptor set layout binding flags: [source,cpp] ---- @@ -170,17 +179,78 @@ void enable_descriptor_indexing(vk::DeviceCreateInfo& device_create_info, device_create_info.setPEnabledExtensionNames(enabled_extensions); vk::PhysicalDeviceDescriptorIndexingFeatures indexing_features{}; + // Shader indexing capabilities (commonly needed alongside robustness) + indexing_features.setShaderSampledImageArrayNonUniformIndexing(VK_TRUE); + indexing_features.setShaderStorageBufferArrayNonUniformIndexing(VK_TRUE); + + // Robustness-enabling behaviors indexing_features.setRuntimeDescriptorArray(VK_TRUE); indexing_features.setDescriptorBindingPartiallyBound(VK_TRUE); indexing_features.setDescriptorBindingSampledImageUpdateAfterBind(VK_TRUE); indexing_features.setDescriptorBindingStorageBufferUpdateAfterBind(VK_TRUE); + indexing_features.setDescriptorBindingUpdateUnusedWhilePending(VK_TRUE); - // Add to the pNext chain + // Add to the pNext chain (can be chained together with VK_EXT_robustness2) indexing_features.pNext = device_create_info.pNext; device_create_info.pNext = &indexing_features; } ---- +For descriptor arrays, you must also specify binding flags at layout creation time: + +[source,cpp] +---- +// Example: descriptor set layout with a runtime-sized array that can be partially bound +vk::DescriptorSetLayoutBinding binding{}; +binding.binding = 0; +binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; +binding.descriptorCount = 128; // example array size; for true runtime arrays also enable variable descriptor counts +binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + +vk::DescriptorBindingFlags binding_flags = + vk::DescriptorBindingFlagBits::ePartiallyBound | + vk::DescriptorBindingFlagBits::eUpdateAfterBind; + +vk::DescriptorSetLayoutBindingFlagsCreateInfo flags_ci{}; +flags_ci.setBindingCount(1); +flags_ci.setPBindingFlags(&binding_flags); + +vk::DescriptorSetLayoutCreateInfo dsl_ci{}; +dsl_ci.setPBindings(&binding); +dsl_ci.setBindingCount(1); +// Required when using update-after-bind flags +// (some descriptor types require pool and layout flags to match update-after-bind usage) +dsl_ci.flags |= vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool; + +dsl_ci.pNext = &flags_ci; + +vk::raii::DescriptorSetLayout set_layout{device, dsl_ci}; +---- + +If you need truly variable-length descriptor arrays at runtime, also enable variable descriptor counts and use the corresponding allocate info: + +[source,cpp] +---- +// Enable the device feature +// indexing_features.setDescriptorBindingVariableDescriptorCount(VK_TRUE); // do this where features are enabled + +uint32_t max_descriptors_for_set0 = 1024; // requested at allocation time + +vk::DescriptorSetVariableDescriptorCountAllocateInfo variable_counts_info{}; +variable_counts_info.setDescriptorSetCount(1); +variable_counts_info.setPDescriptorCounts(&max_descriptors_for_set0); + +vk::DescriptorSetAllocateInfo alloc_info{}; +alloc_info.setDescriptorPool(descriptor_pool); +alloc_info.setDescriptorSetCount(1); +alloc_info.setPSetLayouts(&*set_layout); +alloc_info.pNext = &variable_counts_info; + +auto descriptor_sets = vk::raii::DescriptorSets{device, alloc_info}; +---- + +Note: With VK_EXT_robustness2's nullDescriptor = VK_TRUE and descriptor indexing's partially-bound behavior, unbound array elements will read as zero rather than invoking undefined behavior. + === Combining Robustness Extensions with Debugging Tools For maximum effectiveness, combine robustness extensions with the debugging tools we discussed in previous sections: From 9705b60aeee6a068d39215ecc6286b1037f60dd0 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:52:03 -0700 Subject: [PATCH 079/102] addressing more comments. --- .../03_performance_optimizations.adoc | 65 ------------------- 1 file changed, 65 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index a11b75ec..c9ce8cac 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -8,12 +8,6 @@ Mobile devices have significantly different hardware constraints compared to des === Texture Optimizations -To keep the workflow concrete and digestible, think of texture optimization in three steps: - -1) Step 1: Ensure power‑of‑two (POT) dimensions (resize if needed) -2) Step 2: Pick a hardware‑compressed format supported by the device (ASTC → ETC2 → BC → fallback) -3) Step 3: Upload/update the Vulkan image and clean up staging resources - [NOTE] ==== We focus on mobile‑specific decisions here. For general Vulkan image creation, staging uploads, and descriptor setup, refer back to earlier chapters in the engine series—link:../Engine_Architecture/04_resource_management.adoc[Resource Management], link:../Engine_Architecture/05_rendering_pipeline.adoc[Rendering Pipeline]—or the Vulkan Guide (https://docs.vulkan.org/guide/latest/). @@ -21,65 +15,6 @@ We focus on mobile‑specific decisions here. For general Vulkan image creation, Textures are often the largest consumers of memory in a graphics application. Optimizing them is crucial for mobile performance. -==== Power-of-Two Textures - -One of the most important optimizations for mobile is using power-of-two (POT) texture dimensions (e.g., 256x256, 512x1024, etc.). Here's why: - -1. *Hardware Acceleration*: Most mobile GPUs are optimized for POT textures and can process them more efficiently. - -2. *Mipmapping*: POT textures are required for proper mipmapping, which is essential for performance and visual quality. - -3. *Memory Alignment*: POT textures align better with memory pages, improving memory access patterns. - -To ensure your textures are POT, you can implement a texture loading system that automatically resizes non-POT textures: - -[source,cpp] ----- -vk::Extent2D make_power_of_two(uint32_t width, uint32_t height) { - // Find the next power of two for each dimension - uint32_t pot_width = 1; - while (pot_width < width) pot_width *= 2; - - uint32_t pot_height = 1; - while (pot_height < height) pot_height *= 2; - - return vk::Extent2D(pot_width, pot_height); -} - -void load_texture(const std::string& path) { - // Load the image data - int width, height, channels; - stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha); - - if (!pixels) { - throw std::runtime_error("Failed to load texture image: " + path); - } - - // Check if the dimensions are power of two - bool is_pot = (width & (width - 1)) == 0 && (height & (height - 1)) == 0; - - if (!is_pot) { - // Resize to power of two - vk::Extent2D pot_size = make_power_of_two(width, height); - - stbi_uc* resized_pixels = new stbi_uc[pot_size.width * pot_size.height * 4]; - stbir_resize_uint8(pixels, width, height, 0, - resized_pixels, pot_size.width, pot_size.height, 0, 4); - - stbi_image_free(pixels); - pixels = resized_pixels; - width = pot_size.width; - height = pot_size.height; - } - - // Create Vulkan image and upload pixels - // ... - - // Free the pixel data - stbi_image_free(pixels); -} ----- - ==== Efficient Texture Formats Choosing the right texture format is crucial for mobile performance: From 921eed422faa00737237b1970d9d7ff5482a60d4 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 15:53:16 -0700 Subject: [PATCH 080/102] addressing more comments. --- .../Mobile_Development/03_performance_optimizations.adoc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index c9ce8cac..10b7c88b 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -248,11 +248,6 @@ Different mobile GPU vendors have specific architectures that may benefit from t Different mobile GPU vendors have specific architectures that benefit from targeted optimizations: -* *GPU Technologies*: Many vendors implement custom GPU technologies to improve performance and power efficiency: - - Maintain stable frame rates rather than pushing for maximum frames - - Avoid unnecessary GPU state changes - - Use efficient rendering techniques appropriate for the GPU architecture - * *Memory Management*: Many mobile SoCs have unified memory architecture: - Use `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` memory when possible - Take advantage of fast CPU-GPU memory transfers in unified memory architectures From 2a22079c9c4e3c1d1875f6ee50505372a5300e7f Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 16:04:26 -0700 Subject: [PATCH 081/102] addressing more comments. --- .../Mobile_Development/04_rendering_approaches.adoc | 8 ++++---- .../Mobile_Development/05_vulkan_extensions.adoc | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc index 71352adf..abda1abb 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -99,11 +99,11 @@ vk::RenderPass render_pass = device.createRenderPass(render_pass_info); ==== Best Practices for TBR -* *Avoid Framebuffer Reads*: Reading from the framebuffer can force the tile to be written to main memory and then read back, which is expensive. +* *Avoid External Framebuffer Reads*: Avoid reading from images that require the tile to be flushed to external memory and reloaded; this is expensive on TBR. + - Local, same-pixel reads from on-chip/tile memory are fine and encouraged on tile-based GPUs. + - In Vulkan, use input attachments within subpasses or the `VK_KHR_dynamic_rendering_local_read` capability to perform tile-local reads without leaving tile memory. This is often referred to as pixel-local storage (PLS) on tile-based architectures. -* *Optimize for Tile Size*: Consider the tile size when designing your -rendering algorithm. For example, if you know the tile size is 16x16, you -might organize your data or algorithms to work efficiently with that size. +* *Optimize for Tile Size*: Consider the tile size when designing your rendering algorithm. For example, if you know the tile size is 16x16, you might organize your data or algorithms to work efficiently with that size. ===== Memory Management diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc index 15c4c8ac..bfc32c87 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -12,11 +12,11 @@ Dynamic rendering is a game-changing extension that simplifies the Vulkan render ==== Overview -The `VK_KHR_dynamic_rendering` extension (now part of Vulkan 1.3 core) allows you to begin and end rendering operations directly within a command buffer, without creating render pass and framebuffer objects. This is particularly beneficial for mobile development as it: +The `VK_KHR_dynamic_rendering` extension (now part of Vulkan 1.3 core) allows you to begin and end rendering operations directly within a command buffer, without creating render pass and framebuffer objects. This benefits a wide range of platforms (desktop and mobile) because it: 1. *Simplifies Code*: Reduces the complexity of managing render passes and framebuffers. 2. *Enables More Flexible Rendering*: Makes it easier to implement techniques that don't fit well into the traditional render pass model. -3. *Reduces API Overhead*: Fewer objects to create and manage means less CPU overhead. +3. *Potentially Lowers API Overhead*: Fewer objects to create and manage can simplify setup; any CPU savings are usually small and workload-dependent. ==== Implementation (Step-by-step) @@ -158,11 +158,11 @@ The `VK_KHR_dynamic_rendering_local_read` extension is particularly effective at 1. *Eliminates Tile Flush Operations*: Without this extension, when a shader needs to read from a previously written attachment, the GPU must flush the entire tile to main memory and then read it back. This extension allows the shader to read directly from the tile memory, eliminating these costly flush operations. -2. *Optimizes Post-Processing Effects*: Effects like bloom, tone mapping, and depth-of-field that require reading from rendered images can be performed without leaving the tile memory. +2. *Supports Per-Pixel Local Reads*: It enables fragment shaders to read the value written at the same pixel from attachments within the current rendering scope/tile. This suits per-pixel operations (e.g., tone mapping or reading depth/previous color). -3. *Bandwidth Reduction Measurements*: In real-world applications, this extension has been shown to reduce memory bandwidth by up to 30% for post-processing heavy workloads. This is particularly significant on mobile devices where memory bandwidth is often a bottleneck and directly impacts battery life. +3. *Bandwidth Reduction Measurements*: In real-world applications, this extension has been shown to reduce memory bandwidth for workloads that benefit from per-pixel local reads. The benefit is workload- and GPU-dependent. -4. *Practical Example*: Consider a deferred rendering pipeline that needs to read G-buffer data. Without this extension, the G-buffer would need to be written to main memory and then read back for the lighting pass. With this extension, the lighting pass can read directly from the G-buffer in tile memory, saving significant bandwidth. +4. *Practical Example*: Consider a deferred rendering pipeline that needs to read G-buffer data at the same pixel for lighting. Without this extension, the G-buffer would need to be written to main memory and then read back for the lighting pass. With this extension, the lighting pass can read directly from the G-buffer in tile memory, saving bandwidth. ==== Implementation From 41e336e67ac0680da8f7282c0cfbeccb97b56184 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 16:25:30 -0700 Subject: [PATCH 082/102] addressing more comments. --- .../Mobile_Development/01_introduction.adoc | 2 +- .../05_vulkan_extensions.adoc | 27 ++--- .../Mobile_Development/06_conclusion.adoc | 112 ++++-------------- 3 files changed, 36 insertions(+), 105 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc index 066fd102..ad79de56 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc @@ -12,7 +12,7 @@ Mobile development presents unique challenges and opportunities for Vulkan appli This chapter will guide you through the complex landscape of mobile Vulkan development, where desktop assumptions often don't apply. We'll start by examining the platform-specific requirements of Android and iOS, which present unique challenges in setup, lifecycle management, and input handling. Mobile applications face constraints that desktop applications rarely encounter—sudden interruptions, battery concerns, and varying hardware capabilities all require careful consideration in your engine design. -Performance optimization takes on critical importance in mobile environments where every watt of power consumption and every millisecond of frame time affects user experience. We'll explore essential techniques like power-of-two texture usage and efficient texture formats, along with mobile-specific optimizations that can mean the difference between smooth performance and user frustration. +Performance optimization takes on critical importance in mobile environments where every watt of power consumption and every millisecond of frame time affects user experience. We'll explore essential techniques like efficient texture formats, along with mobile-specific optimizations that can mean the difference between smooth performance and user frustration. Understanding the fundamental architectural differences between mobile and desktop GPUs becomes essential for effective optimization. We'll compare Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR) approaches, helping you understand why techniques that work well on desktop might perform poorly on mobile, and how to design rendering strategies that leverage mobile GPU strengths. diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc index bfc32c87..8db4ca5e 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -148,7 +148,7 @@ This extension enhances dynamic rendering by allowing fragment shaders to read f Key benefits include: -1. *Reduced Memory Bandwidth*: Reads happen from on-chip memory rather than main memory, reducing bandwidth usage by up to 30% in bandwidth-intensive operations. +1. *Reduced Memory Bandwidth*: Reads happen from on-chip memory rather than main memory, reducing bandwidth usage for bandwidth-intensive operations. 2. *Improved Performance*: Particularly for algorithms that need to read from previously written attachments. 3. *Power Efficiency*: Lower memory bandwidth means lower power consumption. @@ -222,7 +222,7 @@ This extension allows shaders to: 1. *Access Tile Memory Directly*: Read and write to the current tile's memory without going through main memory. 2. *Perform Tile-Local Operations*: Execute operations that stay entirely within the tile memory. 3. *Optimize Bandwidth-Intensive Algorithms*: Particularly beneficial for post-processing effects. -4. *Reduce Memory Bandwidth*: Can reduce memory bandwidth usage by up to 30% for rendering workloads that involve multiple passes. +4. *Reduce Memory Bandwidth*: Helps lower memory bandwidth by keeping data in tile-local memory during multi-pass workloads. ==== How It Reduces Memory Bandwidth @@ -232,7 +232,7 @@ The `VK_EXT_shader_tile_image` extension is particularly effective at reducing m 2. *Eliminates Intermediate Memory Transfers*: Without this extension, multi-pass rendering requires writing results to main memory after each pass and reading them back for the next pass. With `VK_EXT_shader_tile_image`, these intermediate results can stay in tile memory, eliminating these costly transfers. -3. *Bandwidth Savings Measurements*: Testing on various mobile GPUs has shown memory bandwidth reductions of up to 30% for complex rendering pipelines that use multiple passes, such as those involving post-processing effects. +3. *Bandwidth Savings Measurements*: Testing on various mobile GPUs has shown meaningful bandwidth reductions for complex multi-pass pipelines; actual gains are workload- and GPU-dependent. 4. *Practical Applications*: - *Image Processing Filters*: Applying multiple filters (blur, sharpen, color correction) can be done without leaving tile memory. @@ -308,36 +308,35 @@ vk::Device device = physical_device.createDevice(device_create_info); // ... ---- -=== Vendor-Specific Extension Support +=== Device Extension Support -Different mobile vendors may have varying levels of support for Vulkan extensions. Understanding these differences can help you optimize your application for specific hardware. +Different mobile vendors and devices vary in which Vulkan extensions they expose. Understanding per-device support helps you pick features safely at runtime. -==== Vendor-Specific Extension Support Details +==== Device Extension Support Details Different mobile GPU vendors have varying levels of support for Vulkan extensions: * *Dynamic Rendering Support*: Many mobile GPUs have optimized implementations of `VK_KHR_dynamic_rendering`. This can lead to significant performance improvements compared to traditional render passes, especially on tile-based renderers. -* *Tile-Based Optimizations*: For devices with tile-based renderers -(including Mali, PowerVR, and many others), extensions like `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are particularly effective. These extensions can reduce memory bandwidth by up to 30% in some scenarios. +* *Tile-Based Optimizations*: On tile-based GPUs (e.g., Mali, PowerVR), `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are effective because they keep reads and writes in tile memory. See the extension sections above for details; benefits are workload- and GPU-dependent. -* *Checking for Vendor-Specific Extension Support*: +* *Checking for Extension Support (EXT/KHR) on the current device*: [source,cpp] ---- -// Common vendor IDs +// Common vendor IDs (used here only for labeling/logging output) const uint32_t VENDOR_ID_QUALCOMM = 0x5143; // Adreno const uint32_t VENDOR_ID_ARM = 0x13B5; // Mali const uint32_t VENDOR_ID_IMAGINATION = 0x1010; // PowerVR const uint32_t VENDOR_ID_HUAWEI = 0x19E5; // Kirin const uint32_t VENDOR_ID_APPLE = 0x106B; // Apple -bool check_vendor_extension_support(vk::PhysicalDevice physical_device) { +bool log_device_extension_support(vk::PhysicalDevice physical_device) { vk::PhysicalDeviceProperties props = physical_device.getProperties(); std::string vendor_name; - // Identify vendor + // Identify vendor for display purposes only switch (props.vendorID) { case VENDOR_ID_QUALCOMM: vendor_name = "Qualcomm"; break; case VENDOR_ID_ARM: vendor_name = "ARM Mali"; break; @@ -347,7 +346,7 @@ bool check_vendor_extension_support(vk::PhysicalDevice physical_device) { default: vendor_name = "Unknown"; break; } - // Check for extensions that work well on mobile devices + // Check for widely useful EXT/KHR extensions on this device auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); bool has_dynamic_rendering = false; bool has_dynamic_rendering_local_read = false; @@ -374,7 +373,7 @@ bool check_vendor_extension_support(vk::PhysicalDevice physical_device) { } ---- -* *Vendor-Specific Optimizations*: When developing for mobile devices, +* *Platform-Specific Optimizations*: When developing for mobile devices, consider these optimizations: - Prioritize the use of dynamic rendering over traditional render passes on tile-based renderers - Use tile-based extensions whenever available diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc index 265a7976..fca97fb2 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -4,61 +4,6 @@ == Conclusion -In this chapter, we've explored the key aspects of adapting your Vulkan engine for mobile platforms. Let's summarize what we've learned and discuss how to apply these techniques in your own projects. - -=== What We've Learned - -==== Platform Considerations for Android and iOS - -We started by examining the specific requirements and constraints of developing Vulkan applications for Android and iOS: - -* Setting up Vulkan on Android using the NDK -* Using MoltenVK to bring Vulkan to iOS -* Managing the complex lifecycle of mobile applications -* Handling platform-specific input methods -* Creating cross-platform abstractions to maintain a single codebase -* Addressing vendor-specific considerations like custom Android versions and alternative app stores - -Understanding these platform-specific considerations is essential for creating a robust mobile application that behaves correctly across different devices and operating systems. - -==== Performance Optimizations for Mobile - -We then explored key performance optimizations for mobile hardware: - -* Using power-of-two textures for better hardware acceleration and memory alignment -* Selecting efficient texture formats like ASTC, ETC2, and PVRTC -* Minimizing memory allocations through pooling and suballocation -* Reducing bandwidth usage with optimized vertex formats and smaller data types -* Optimizing draw calls through instancing, batching, and LOD systems -* Leveraging vendor-specific optimizations for different mobile GPU architectures - -These optimizations help address the limited resources available on mobile devices, ensuring your application runs smoothly while conserving battery life. - -==== Rendering Approaches: TBR vs IMR - -Next, we compared the two main rendering architectures found in mobile GPUs: - -* Tile-Based Rendering (TBR): How it works, its advantages, and specific optimizations -* Immediate Mode Rendering (IMR): How it works, its advantages, and specific optimizations -* Detecting the rendering architecture and adapting your engine accordingly -* Best practices that work well for both architectures -* Identifying TBR GPUs from major vendors including Qualcomm, PowerVR, ARM Mali, Apple, and Huawei - -Understanding these different rendering approaches allows you to optimize your engine for the specific hardware it's running on, maximizing performance across a wide range of devices. - -==== Vulkan Extensions for Mobile - -Finally, we explored Vulkan extensions that can significantly improve performance on mobile devices: - -* VK_KHR_dynamic_rendering: Simplifying the rendering workflow -* VK_KHR_dynamic_rendering_local_read: Enabling efficient reads from attachments in tile memory -* VK_EXT_shader_tile_image: Providing direct access to tile memory in shaders -* Combining these extensions for maximum performance -* Best practices for using extensions in a cross-platform engine -* Vendor-specific extension support and how different mobile GPU vendors implement these extensions - -These extensions leverage the unique characteristics of mobile GPUs, particularly tile-based renderers, to achieve better performance and power efficiency. - === Putting It All Together Let's see how all these components can work together in a complete mobile-optimized Vulkan application: @@ -216,51 +161,38 @@ private: }; ---- -=== Best Practices for Mobile Vulkan Development - -Based on what we've covered in this chapter, here are some best practices for mobile Vulkan development: - -1. *Design for Platform Differences*: Create abstractions that handle platform-specific differences while maintaining a single core codebase. - -2. *Optimize for Limited Resources*: Always consider the limited memory, bandwidth, and power available on mobile devices. - -3. *Adapt to the Rendering Architecture*: Optimize your rendering pipeline based on whether the device uses TBR or IMR. +=== Ship-Ready Checklist -4. *Use Hardware-Accelerated Formats*: Choose texture formats that are natively supported by the hardware. +1. Feature detection and fallbacks: Probe EXT/KHR support at startup, enable conditionally, and maintain tested fallback paths. +2. Render path selection: Switch between TBR-friendly and IMR-neutral paths at runtime based on a simple vendor/heuristic check. +3. Framebuffer read policy: Prefer tile-local, per-pixel reads (input attachments or dynamic rendering local read). Avoid patterns that force external memory round-trips. +4. Textures and assets: Use KTX2 as the container; prefer ASTC when available with ETC2/PVRTC fallbacks as needed. Generate mipmaps offline. +5. Memory/attachments: Use transient attachments where results aren’t needed after the pass; suballocate to minimize fragmentation. +6. Thermal/perf governor: Implement dynamic resolution or quality tiers and sensible FPS caps to keep thermals in check. +7. Instrumentation: Add GPU markers/timestamps, frame-time histograms, and bandwidth proxies to track regressions. +8. Device matrix: Maintain a small, representative device lab (different vendors/tiers) and run sanity scenes regularly. -5. *Leverage Vulkan Extensions*: Take advantage of mobile-specific extensions when available. +=== Validation and Profiling Playbook -6. *Test on Real Devices*: Emulators and simulators don't accurately represent real-world performance. +- Validate correctness: + * Swapchain details (present mode, min image count) per device. + * Layout transitions and access masks, especially when using local read. + * Synchronization between rendering scopes and compute/transfer work. +- Profile efficiently: + * Use platform tools (e.g., Android GPU Inspector, RenderDoc, Xcode GPU Capture) to identify tile flushes, overdraw, and bandwidth hot spots. + * A/B test: classic render pass vs dynamic rendering, local read on/off, tile-image on/off. + * Track power and thermals over multi‑minute runs, not just single frames. -7. *Monitor Performance Metrics*: Track frame times, memory usage, and power consumption to identify bottlenecks. +=== Next Steps -8. *Provide Quality Options*: Allow users to adjust quality settings based on their device's capabilities. - -=== Future Directions - -Mobile graphics hardware continues to evolve rapidly. Here are some trends to watch: - -* *Unified Memory Architectures*: More mobile SoCs are adopting unified memory, which can change how you optimize memory access. -* *Ray Tracing on Mobile*: As ray tracing hardware becomes more common on mobile devices, new optimization techniques will emerge. -* *AI-Enhanced Rendering*: Mobile GPUs are increasingly incorporating AI acceleration, which can be leveraged for rendering tasks. -* *Cross-Platform Development*: Tools and frameworks for cross-platform development continue to improve, making it easier to target multiple platforms. -* *Mobile GPU Innovations*: Mobile GPU vendors continue to advance their technologies with each generation, introducing new features and optimizations that can be leveraged through Vulkan. - -=== Final Thoughts - -Developing for mobile platforms presents unique challenges, but also offers exciting opportunities to reach a wider audience. By understanding the specific characteristics of mobile hardware and optimizing your Vulkan engine accordingly, you can create high-performance applications that provide a great user experience while efficiently using the limited resources available on mobile devices. - -Remember that mobile optimization is an ongoing process. As new devices, architectures, and extensions emerge, continue to refine your engine to take advantage of these advancements. +- Integrate a capability layer that exposes feature bits (dynamic rendering, local read, tile image) to higher-level systems. +- Add automated startup probes that dump device/feature info to logs for field telemetry. +- Expand the regression scene suite to cover TBR‑sensitive and bandwidth‑heavy paths. === Code Examples The complete code for this chapter can be found in the following files: -* `simple_engine/36_mobile_platform_integration.cpp`: Implementation of platform-specific integration for Android and iOS -* `simple_engine/37_mobile_optimizations.cpp`: Implementation of performance optimizations for mobile -* `simple_engine/38_tbr_optimizations.cpp`: Implementation of optimizations for tile-based renderers -* `simple_engine/39_mobile_extensions.cpp`: Implementation of mobile-specific Vulkan extensions - link:../../attachments/simple_engine/36_mobile_platform_integration.cpp[Mobile Platform Integration C{pp} code] link:../../attachments/simple_engine/37_mobile_optimizations.cpp[Mobile Optimizations C{pp} code] link:../../attachments/simple_engine/38_tbr_optimizations.cpp[TBR Optimizations C{pp} code] From b78f8b292601f753349b93e7bfe58267cbb8bea6 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 20:48:04 -0700 Subject: [PATCH 083/102] addressing more comments. --- .../03_performance_optimizations.adoc | 107 ++++++++++-------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 10b7c88b..4d0e9386 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -13,22 +13,27 @@ Mobile devices have significantly different hardware constraints compared to des We focus on mobile‑specific decisions here. For general Vulkan image creation, staging uploads, and descriptor setup, refer back to earlier chapters in the engine series—link:../Engine_Architecture/04_resource_management.adoc[Resource Management], link:../Engine_Architecture/05_rendering_pipeline.adoc[Rendering Pipeline]—or the Vulkan Guide (https://docs.vulkan.org/guide/latest/). ==== -Textures are often the largest consumers of memory in a graphics application. Optimizing them is crucial for mobile performance. +Textures are often the largest consumers of memory in a graphics application. Optimizing them is crucial for performance on both mobile and desktop. ==== Efficient Texture Formats -Choosing the right texture format is crucial for mobile performance: +Choosing the right texture format is crucial across platforms; what differs is which formats are natively supported by a given device/driver: 1. *Compressed Formats*: Use hardware-supported compressed formats whenever possible: - - *ASTC* (Adaptive Scalable Texture Compression): Widely supported on modern mobile GPUs, offers excellent quality-to-size ratio with flexible block sizes. - - *ETC2/EAC*: Required for OpenGL ES 3.0 and supported by most Android devices. + - *ASTC* (Adaptive Scalable Texture Compression): Widely supported on modern mobile GPUs and increasingly available on desktop; excellent quality-to-size ratio with flexible block sizes. + - *ETC2/EAC*: Required for OpenGL ES 3.0 and supported by most Android devices; commonly available on Vulkan stacks, too. - *PVRTC*: Primarily supported on iOS devices with PowerVR GPUs. - - *BC* (Block Compression): More common on desktop but supported by some high-end mobile GPUs. + - *BC* (Block Compression, a.k.a. DXT/BCn): Ubiquitous on desktop; supported by some mobile GPUs. -2. *Format Selection Based on Content*: Choose formats based on the type of texture: - - Use ASTC 4x4 or 6x6 for normal maps and detailed textures. - - Use ASTC 8x8 or 12x12 for diffuse/albedo textures. - - Consider using R8 for single-channel textures like height maps. +2. *Format Selection Based on Content and Support*: Choose formats based on the type of texture and what the device reports: + - For high detail (normals, roughness): prefer ASTC 4x4 or 6x6 when supported; on desktop, BC5/BC7 are common alternatives. + - For albedo/basecolor: ASTC 6x6–8x8 works well when available; on desktop, BC1/BC7 are typical. + - For single-channel data: consider R8 or compressed single-channel alternatives when available. + +[NOTE] +==== +This guidance is not mobile-only: block compression reduces memory footprint and bandwidth on all platforms. The Mobile chapter highlights it because bandwidth and power are tighter constraints on phones/tablets. On desktop, the same benefits apply; the primary difference is which formats are commonly available (e.g., BC on desktop, ASTC/ETC2 on many mobile devices). +==== Here's how to check for and use compressed formats in Vulkan: @@ -61,57 +66,67 @@ vk::Format find_supported_format(vk::PhysicalDevice physical_device, } vk::Format find_best_compressed_format(vk::PhysicalDevice physical_device) { - // Try ASTC formats first (best quality-to-size ratio) + // Choose based on reported feature support; benefits apply on mobile and desktop. + vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); + + // Candidate lists std::vector astc_formats = { vk::Format::eAstc8x8SrgbBlock, vk::Format::eAstc6x6SrgbBlock, vk::Format::eAstc4x4SrgbBlock }; - - try { - return find_supported_format( - physical_device, - astc_formats, - vk::ImageTiling::eOptimal, - vk::FormatFeatureFlagBits::eSampledImage - ); - } catch (const std::runtime_error&) { - // ASTC not supported, try ETC2 - } - + std::vector bc_formats = { + vk::Format::eBc7SrgbBlock, + vk::Format::eBc3SrgbBlock, + vk::Format::eBc1SrgbBlock + }; std::vector etc2_formats = { vk::Format::eEtc2R8G8B8A8SrgbBlock, vk::Format::eEtc2R8G8B8SrgbBlock }; - try { - return find_supported_format( - physical_device, - etc2_formats, - vk::ImageTiling::eOptimal, - vk::FormatFeatureFlagBits::eSampledImage - ); - } catch (const std::runtime_error&) { - // ETC2 not supported, try BC + // Build a simple preference order: + // - If only BC is supported, try BC first (typical on desktop-only stacks). + // - If ASTC is supported, try ASTC (common on mobile; also available on some desktop drivers). + // - Then try the remaining supported families. + std::vector> preference; + + if (features.textureCompressionBC && !features.textureCompressionASTC_LDR) { + preference.push_back(bc_formats); + } + if (features.textureCompressionASTC_LDR) { + preference.push_back(astc_formats); + } + if (features.textureCompressionBC && features.textureCompressionASTC_LDR) { + // Both supported; add BC as alternative if not already first + if (preference.empty() || preference[0] != bc_formats) { + preference.push_back(bc_formats); + } + } + if (features.textureCompressionETC2) { + preference.push_back(etc2_formats); } - std::vector bc_formats = { - vk::Format::eBc7SrgbBlock, - vk::Format::eBc3SrgbBlock, - vk::Format::eBc1SrgbBlock - }; + // As a fallback, try all families regardless of feature bits (some drivers may expose formats via properties). + if (preference.empty()) { + preference = { astc_formats, etc2_formats, bc_formats }; + } - try { - return find_supported_format( - physical_device, - bc_formats, - vk::ImageTiling::eOptimal, - vk::FormatFeatureFlagBits::eSampledImage - ); - } catch (const std::runtime_error&) { - // Fall back to uncompressed - return vk::Format::eR8G8B8A8Srgb; + for (const auto& family : preference) { + try { + return find_supported_format( + physical_device, + family, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eSampledImage + ); + } catch (const std::runtime_error&) { + // Try next family + } } + + // Fall back to uncompressed + return vk::Format::eR8G8B8A8Srgb; } ---- From 5c7a132c11e7f9adcabc0d8cf86eca16ebb430bf Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 21:04:42 -0700 Subject: [PATCH 084/102] addressing more comments. --- .../03_performance_optimizations.adoc | 77 +------------------ 1 file changed, 1 insertion(+), 76 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 4d0e9386..3859b33a 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -64,70 +64,6 @@ vk::Format find_supported_format(vk::PhysicalDevice physical_device, throw std::runtime_error("Failed to find supported format"); } - -vk::Format find_best_compressed_format(vk::PhysicalDevice physical_device) { - // Choose based on reported feature support; benefits apply on mobile and desktop. - vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); - - // Candidate lists - std::vector astc_formats = { - vk::Format::eAstc8x8SrgbBlock, - vk::Format::eAstc6x6SrgbBlock, - vk::Format::eAstc4x4SrgbBlock - }; - std::vector bc_formats = { - vk::Format::eBc7SrgbBlock, - vk::Format::eBc3SrgbBlock, - vk::Format::eBc1SrgbBlock - }; - std::vector etc2_formats = { - vk::Format::eEtc2R8G8B8A8SrgbBlock, - vk::Format::eEtc2R8G8B8SrgbBlock - }; - - // Build a simple preference order: - // - If only BC is supported, try BC first (typical on desktop-only stacks). - // - If ASTC is supported, try ASTC (common on mobile; also available on some desktop drivers). - // - Then try the remaining supported families. - std::vector> preference; - - if (features.textureCompressionBC && !features.textureCompressionASTC_LDR) { - preference.push_back(bc_formats); - } - if (features.textureCompressionASTC_LDR) { - preference.push_back(astc_formats); - } - if (features.textureCompressionBC && features.textureCompressionASTC_LDR) { - // Both supported; add BC as alternative if not already first - if (preference.empty() || preference[0] != bc_formats) { - preference.push_back(bc_formats); - } - } - if (features.textureCompressionETC2) { - preference.push_back(etc2_formats); - } - - // As a fallback, try all families regardless of feature bits (some drivers may expose formats via properties). - if (preference.empty()) { - preference = { astc_formats, etc2_formats, bc_formats }; - } - - for (const auto& family : preference) { - try { - return find_supported_format( - physical_device, - family, - vk::ImageTiling::eOptimal, - vk::FormatFeatureFlagBits::eSampledImage - ); - } catch (const std::runtime_error&) { - // Try next family - } - } - - // Fall back to uncompressed - return vk::Format::eR8G8B8A8Srgb; -} ---- === Memory Optimizations @@ -284,21 +220,10 @@ vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) { vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); // Check for ASTC support (widely supported on modern mobile GPUs) + // Most games are written with knowledge of what the assets were compressed with so it's standard practice to only ensure the required format is supported. if (features.textureCompressionASTC_LDR) { return vk::Format::eAstc8x8SrgbBlock; } - - // Check for vendor-specific optimizations - // Huawei devices (Mali GPUs) - if (props.vendorID == 0x19E5) { - // Check for ETC2 support as fallback - if (supports_texture_format(physical_device, vk::Format::eEtc2R8G8B8A8SrgbBlock)) { - return vk::Format::eEtc2R8G8B8A8SrgbBlock; - } - } - - // Otherwise, fall back to the general format selection - return find_best_compressed_format(physical_device); } ---- From 560b9f37c50f39fb345f0b9fbd447d7d65ea4cbe Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 21:24:31 -0700 Subject: [PATCH 085/102] addressing more comments. --- .../03_performance_optimizations.adoc | 15 ++++ .../04_rendering_approaches.adoc | 90 ++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 3859b33a..83bcf984 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -6,6 +6,11 @@ Mobile devices have significantly different hardware constraints compared to desktop systems. In this section, we'll explore key performance optimizations that are essential for achieving good performance on mobile platforms. +[NOTE] +==== +This chapter covers general mobile performance. For practices that arise specifically because the GPU is tile-based (TBR), see link:04_rendering_approaches.adoc[Rendering Approaches: Tile-Based Rendering]. +==== + === Texture Optimizations [NOTE] @@ -181,6 +186,11 @@ struct OptimizedVertex { }; ---- +[NOTE] +==== +If you are targeting tile-based GPUs (TBR), bandwidth can be heavily impacted by attachment load/store behavior and tile flushes. See link:04_rendering_approaches.adoc[Rendering Approaches] — sections “Attachment Load/Store Operations on Tilers” and “Pipelining on Tilers: Subpass Dependencies and BY_REGION” for concrete guidance. +==== + === Draw Call Optimizations Mobile GPUs are particularly sensitive to draw call overhead: @@ -191,6 +201,11 @@ Mobile GPUs are particularly sensitive to draw call overhead: 3. *Level of Detail (LOD)*: Implement LOD systems to reduce geometry complexity for distant objects. +[NOTE] +==== +On tile-based GPUs, reducing CPU overhead is important, but keeping work and data on-chip via proper pipelining and subpasses often yields larger gains. See link:04_rendering_approaches.adoc[Rendering Approaches] — “Pipelining on Tilers: Subpass Dependencies and BY_REGION” for barrier/subpass patterns, and “Attachment Load/Store Operations on Tilers” for loadOp/storeOp guidance that avoids external memory traffic. +==== + === Vendor-Specific Optimizations Different mobile GPU vendors have specific architectures that may benefit from targeted optimizations. diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc index abda1abb..e1a5db0c 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -47,7 +47,7 @@ depth_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); depth_attachment.setInitialLayout(vk::ImageLayout::eUndefined); depth_attachment.setFinalLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal); -// When creating the image, use the transient flag +// When creating the image, mark the attachment as transient vk::ImageCreateInfo image_info{}; image_info.setImageType(vk::ImageType::e2D); image_info.setExtent(vk::Extent3D(width, height, 1)); @@ -56,9 +56,10 @@ image_info.setArrayLayers(1); image_info.setFormat(depth_format); image_info.setTiling(vk::ImageTiling::eOptimal); image_info.setInitialLayout(vk::ImageLayout::eUndefined); -image_info.setUsage(vk::ImageUsageFlagBits::eDepthStencilAttachment); +image_info.setUsage(vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eTransientAttachment); image_info.setSamples(vk::SampleCountFlagBits::e1); -image_info.setFlags(vk::ImageCreateFlagBits::eTransient); // Transient flag +// Prefer lazily allocated memory for transient attachments when supported +// Choose memory with vk::MemoryPropertyFlagBits::eLazilyAllocated ---- * *Render Pass Structure*: Design your render passes to take advantage of @@ -105,6 +106,89 @@ vk::RenderPass render_pass = device.createRenderPass(render_pass_info); * *Optimize for Tile Size*: Consider the tile size when designing your rendering algorithm. For example, if you know the tile size is 16x16, you might organize your data or algorithms to work efficiently with that size. +===== Attachment Load/Store Operations on Tilers + +On tile-based GPUs, correctly using loadOp and storeOp is one of the highest-impact optimizations: + +- Clear attachments with loadOp = CLEAR and initialLayout = UNDEFINED when you don't need previous contents. This avoids an external memory read for the tile. +- Use storeOp = DONT_CARE for attachments whose results are not needed after the render pass (e.g., transient depth or intermediate color targets). This can prevent flushing the tile back to main memory. +- For the swapchain image (or any image you will sample/transfer from later), use storeOp = STORE and set finalLayout appropriately (e.g., PRESENT_SRC_KHR for the swapchain). +- For MSAA, resolve within the same render pass so the hardware can resolve from tile memory and only store the resolved image to external memory. + +[source,cpp] +---- +// Color attachment that we clear and present +vk::AttachmentDescription color_attachment{}; +color_attachment.setFormat(swapchain_format); +color_attachment.setSamples(vk::SampleCountFlagBits::e1); +color_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +color_attachment.setStoreOp(vk::AttachmentStoreOp::eStore); // we need to present +color_attachment.setStencilLoadOp(vk::AttachmentLoadOp::eDontCare); +color_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); +color_attachment.setInitialLayout(vk::ImageLayout::eUndefined); // no need to load previous contents +color_attachment.setFinalLayout(vk::ImageLayout::ePresentSrcKHR); + +// Depth attachment used only within the pass +vk::AttachmentDescription depth_attachment{}; +depth_attachment.setFormat(depth_format); +depth_attachment.setSamples(vk::SampleCountFlagBits::e1); +depth_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +depth_attachment.setStoreOp(vk::AttachmentStoreOp::eDontCare); // don't flush depth to memory +depth_attachment.setStencilLoadOp(vk::AttachmentLoadOp::eDontCare); +depth_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); +depth_attachment.setInitialLayout(vk::ImageLayout::eUndefined); +depth_attachment.setFinalLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal); +---- + +[NOTE] +==== +If you use dynamic rendering, the same rules apply via vk::RenderingAttachmentInfo loadOp/storeOp fields. +See Vulkan Guide for background: Render Passes and Subpasses, Tile-based GPUs. +==== + +===== Pipelining on Tilers: Subpass Dependencies and BY_REGION + +Tile-based GPUs benefit from fine-grained synchronization that keeps work and data on-chip: + +- Prefer subpasses with input attachments to keep producer/consumer within the same render pass, enabling tile-local reads. +- Use vk::DependencyFlagBits::eByRegion to scope hazards to the pixel regions actually written/read, avoiding unnecessary tile flushes. +- Avoid over-broad barriers (e.g., ALL_COMMANDS, MEMORY_READ/WRITE) that serialize the pipeline and may force external memory traffic. Use precise stage/access masks. + +Example: dependency from a color-writing subpass to a subpass that reads that color as an input attachment. + +[source,cpp] +---- +vk::SubpassDependency dep{}; +dep.setSrcSubpass(0); +dep.setDstSubpass(1); +dep.setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); +dep.setDstStageMask(vk::PipelineStageFlagBits::eFragmentShader); +dep.setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); +dep.setDstAccessMask(vk::AccessFlagBits::eInputAttachmentRead); +dep.setDependencyFlags(vk::DependencyFlagBits::eByRegion); +---- + +Example: external dependency to the first subpass of a render pass, allowing pipelining with prior pass while limiting scope by region. + +[source,cpp] +---- +vk::SubpassDependency externalDep{}; +externalDep.setSrcSubpass(VK_SUBPASS_EXTERNAL); +externalDep.setDstSubpass(0); +externalDep.setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); +externalDep.setDstStageMask(vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eColorAttachmentOutput); +externalDep.setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); +externalDep.setDstAccessMask(vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eColorAttachmentWrite); +externalDep.setDependencyFlags(vk::DependencyFlagBits::eByRegion); +---- + +[NOTE] +==== +With Synchronization2 (vkCmdPipelineBarrier2 and friends) avoid ALL_COMMANDS and prefer the minimal set of stages/access that capture your hazard. Use render pass/subpass structure when possible—it's the most tiler-friendly way to express pipelining. +==== + +For further guidance, see the xref:https://docs.vulkan.org/guide/latest/[Vulkan Guide] topics on Tile-based GPUs, Render Passes, and Synchronization. + ===== Memory Management To improve the efficiency of memory allocation in TBR architectures: From 6db6b9dd691c3d798ee7707dd85206f22f71d964 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 21:28:56 -0700 Subject: [PATCH 086/102] addressing more comments. --- .../Mobile_Development/03_performance_optimizations.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc index 83bcf984..ed9dcdc1 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -73,7 +73,7 @@ vk::Format find_supported_format(vk::PhysicalDevice physical_device, === Memory Optimizations -Memory is a precious resource on mobile devices. Here are some key optimizations: +Memory is a precious resource on all platforms. It tends to be more performance‑critical on mobile due to tighter bandwidth, power, and thermal budgets. Here are some key optimizations: ==== Minimize Memory Allocations From 02baf4d75de361a35734034c030ad5f86c6a7378 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 22:58:23 -0700 Subject: [PATCH 087/102] addressing more comments. --- attachments/simple_engine/audio_system.cpp | 2 +- attachments/simple_engine/engine.cpp | 62 +++++++++++----------- attachments/simple_engine/engine.h | 22 +++----- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index df283be4..c8ae5136 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -587,7 +587,7 @@ void AudioSystem::Update(float deltaTime) { // Synchronize HRTF listener position and orientation with active camera if (engine) { - CameraComponent* activeCamera = engine->GetActiveCamera(); + const CameraComponent* activeCamera = engine->GetActiveCamera(); if (activeCamera) { // Get camera position glm::vec3 cameraPos = activeCamera->GetPosition(); diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index f56f8ada..23177cad 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -114,9 +114,7 @@ void Engine::Run() { running = true; // Main loop - int loopCount = 0; while (running) { - loopCount++; // Process platform events if (!platform->ProcessEvents()) { running = false; @@ -180,17 +178,16 @@ void Engine::Cleanup() { } Entity* Engine::CreateEntity(const std::string& name) { - // Check if an entity with this name already exists - if (entityMap.contains(name)) { - return nullptr; - } - + // Always allow duplicate names; map stores a representative entity // Create the entity auto entity = std::make_unique(name); - // Add to the map and vector + // Add to the vector and map entities.push_back(std::move(entity)); + Entity* rawPtr = entities.back().get(); + // Update the map to point to the most recently created entity with this name + entityMap[name] = rawPtr; - return entities.back().get(); + return rawPtr; } Entity* Engine::GetEntity(const std::string& name) const { @@ -206,6 +203,9 @@ bool Engine::RemoveEntity(Entity* entity) { return false; } + // Remember the name before erasing ownership + std::string name = entity->GetName(); + // Find the entity in the vector auto it = std::find_if(entities.begin(), entities.end(), [entity](const std::unique_ptr& e) { @@ -213,12 +213,21 @@ bool Engine::RemoveEntity(Entity* entity) { }); if (it != entities.end()) { - // Remove from the map - entityMap.erase(entity->GetName()); - - // Remove from the vector + // Remove from the vector (ownership) entities.erase(it); + // Update the map: point to another entity with the same name if one exists + auto remainingIt = std::find_if(entities.begin(), entities.end(), + [&name](const std::unique_ptr& e) { + return e->GetName() == name; + }); + + if (remainingIt != entities.end()) { + entityMap[name] = remainingIt->get(); + } else { + entityMap.erase(name); + } + return true; } @@ -233,50 +242,39 @@ bool Engine::RemoveEntity(const std::string& name) { return false; } -std::vector Engine::GetAllEntities() const { - std::vector result; - result.reserve(entities.size()); - - for (const auto& entity : entities) { - result.push_back(entity.get()); - } - - return result; -} - void Engine::SetActiveCamera(CameraComponent* cameraComponent) { activeCamera = cameraComponent; } -CameraComponent* Engine::GetActiveCamera() const { +const CameraComponent* Engine::GetActiveCamera() const { return activeCamera; } -ResourceManager* Engine::GetResourceManager() const { +const ResourceManager* Engine::GetResourceManager() const { return resourceManager.get(); } -Platform* Engine::GetPlatform() const { +const Platform* Engine::GetPlatform() const { return platform.get(); } -Renderer* Engine::GetRenderer() const { +Renderer* Engine::GetRenderer() { return renderer.get(); } -ModelLoader* Engine::GetModelLoader() const { +ModelLoader* Engine::GetModelLoader() { return modelLoader.get(); } -AudioSystem* Engine::GetAudioSystem() const { +const AudioSystem* Engine::GetAudioSystem() const { return audioSystem.get(); } -PhysicsSystem* Engine::GetPhysicsSystem() const { +PhysicsSystem* Engine::GetPhysicsSystem() { return physicsSystem.get(); } -ImGuiSystem* Engine::GetImGuiSystem() const { +const ImGuiSystem* Engine::GetImGuiSystem() const { return imguiSystem.get(); } diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 71537ea9..dcb68a25 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -81,12 +81,6 @@ class Engine { */ bool RemoveEntity(const std::string& name); - /** - * @brief Get all entities. - * @return A vector of pointers to all entities. - */ - std::vector GetAllEntities() const; - /** * @brief Set the active camera. * @param cameraComponent The camera component to set as active. @@ -97,49 +91,49 @@ class Engine { * @brief Get the active camera. * @return A pointer to the active camera component, or nullptr if none is set. */ - CameraComponent* GetActiveCamera() const; + const CameraComponent* GetActiveCamera() const; /** * @brief Get the resource manager. * @return A pointer to the resource manager. */ - ResourceManager* GetResourceManager() const; + const ResourceManager* GetResourceManager() const; /** * @brief Get the platform. * @return A pointer to the platform. */ - Platform* GetPlatform() const; + const Platform* GetPlatform() const; /** * @brief Get the renderer. * @return A pointer to the renderer. */ - Renderer* GetRenderer() const; + Renderer* GetRenderer(); /** * @brief Get the model loader. * @return A pointer to the model loader. */ - ModelLoader* GetModelLoader() const; + ModelLoader* GetModelLoader(); /** * @brief Get the audio system. * @return A pointer to the audio system. */ - AudioSystem* GetAudioSystem() const; + const AudioSystem* GetAudioSystem() const; /** * @brief Get the physics system. * @return A pointer to the physics system. */ - PhysicsSystem* GetPhysicsSystem() const; + PhysicsSystem* GetPhysicsSystem(); /** * @brief Get the ImGui system. * @return A pointer to the ImGui system. */ - ImGuiSystem* GetImGuiSystem() const; + const ImGuiSystem* GetImGuiSystem() const; /** * @brief Handles mouse input for interaction and camera control. From 0b0dddb927221885a43892a0ad0bf378f0725bb8 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 23:11:07 -0700 Subject: [PATCH 088/102] addressing more comments. --- attachments/simple_engine/engine.cpp | 10 +--------- attachments/simple_engine/renderer.h | 3 ++- attachments/simple_engine/renderer_rendering.cpp | 8 ++++++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 23177cad..9c9a531e 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -411,16 +411,8 @@ void Engine::Render() { return; } - // Get all active entities - std::vector activeEntities; - for (auto& entity : entities) { - if (entity->IsActive()) { - activeEntities.push_back(entity.get()); - } - } - // Render the scene (ImGui will be rendered within the render pass) - renderer->Render(activeEntities, activeCamera, imguiSystem.get()); + renderer->Render(entities, activeCamera, imguiSystem.get()); } float Engine::CalculateDeltaTime() { diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index a26242e8..b2cd879d 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "platform.h" #include "entity.h" @@ -135,7 +136,7 @@ class Renderer { * @param camera The camera to use for rendering. * @param imguiSystem The ImGui system for UI rendering (optional). */ - void Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem = nullptr); + void Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem = nullptr); /** * @brief Wait for the device to be idle. diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 5a459be6..74620f8a 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -504,7 +504,7 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity } // Render the scene -void Renderer::Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { +void Renderer::Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { // Set rendering active to prevent memory pool growth during rendering if (memoryPool) { memoryPool->setRenderingActive(true); @@ -608,7 +608,11 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam vk::raii::PipelineLayout* currentLayout = nullptr; // Render each entity - for (auto entity : entities) { + for (auto const& uptr : entities) { + Entity* entity = uptr.get(); + if (!entity || !entity->IsActive()) { + continue; + } // Check if ball-only rendering is enabled and filter entities accordingly if (imguiSystem && imguiSystem->IsBallOnlyRenderingEnabled()) { // Only render entities whose names contain "Ball_" From ccb5c1567b62b99d191aacc6f3402532d53d4bf9 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 18 Aug 2025 19:32:25 -0700 Subject: [PATCH 089/102] addressing more comments. --- attachments/simple_engine/audio_system.cpp | 316 ++++++++++++------- attachments/simple_engine/audio_system.h | 14 +- attachments/simple_engine/component.cpp | 1 - attachments/simple_engine/component.h | 4 +- attachments/simple_engine/engine.cpp | 33 +- attachments/simple_engine/engine.h | 18 +- attachments/simple_engine/entity.cpp | 2 +- attachments/simple_engine/entity.h | 3 +- attachments/simple_engine/imgui_system.cpp | 29 +- attachments/simple_engine/physics_system.cpp | 8 +- attachments/simple_engine/physics_system.h | 7 +- 11 files changed, 257 insertions(+), 178 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index c8ae5136..6d90bd24 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -11,6 +11,7 @@ #include #include #include +#include // OpenAL headers #ifdef __APPLE__ @@ -62,8 +63,9 @@ class ConcreteAudioSource : public AudioSource { void Play() override { playing = true; playbackPosition = 0; - delayTimer = 0.0f; + delayTimer = std::chrono::milliseconds(0); inDelayPhase = false; + sampleAccumulator = 0.0; } void Pause() override { @@ -73,8 +75,9 @@ class ConcreteAudioSource : public AudioSource { void Stop() override { playing = false; playbackPosition = 0; - delayTimer = 0.0f; + delayTimer = std::chrono::milliseconds(0); inDelayPhase = false; + sampleAccumulator = 0.0; } void SetVolume(float volume) override { @@ -106,7 +109,7 @@ class ConcreteAudioSource : public AudioSource { audioLengthSamples = lengthInSamples; } - void UpdatePlayback(float deltaTime, uint32_t samplesProcessed) { + void UpdatePlayback(std::chrono::milliseconds deltaTime, uint32_t samplesProcessed) { if (!playing) return; if (inDelayPhase) { @@ -116,7 +119,7 @@ class ConcreteAudioSource : public AudioSource { // Delay finished, restart playback inDelayPhase = false; playbackPosition = 0; - delayTimer = 0.0f; + delayTimer = std::chrono::milliseconds(0); } } else { // Normal playback, update position @@ -127,7 +130,7 @@ class ConcreteAudioSource : public AudioSource { if (loop) { // Start the delay phase before looping inDelayPhase = true; - delayTimer = 0.0f; + delayTimer = std::chrono::milliseconds(0); } else { // Stop playing if not looping playing = false; @@ -153,6 +156,14 @@ class ConcreteAudioSource : public AudioSource { return position; } + [[nodiscard]] double GetSampleAccumulator() const { + return sampleAccumulator; + } + + void SetSampleAccumulator(double value) { + sampleAccumulator = value; + } + private: std::string name; bool playing = false; @@ -164,9 +175,10 @@ class ConcreteAudioSource : public AudioSource { // Delay and timing functionality uint32_t playbackPosition = 0; // Current position in samples uint32_t audioLengthSamples = 0; // Total length of audio in samples - float delayTimer = 0.0f; // Timer for delay between loops + std::chrono::milliseconds delayTimer = std::chrono::milliseconds(0); // Timer for delay between loops bool inDelayPhase = false; // Whether we're currently in the delay phase - static constexpr float delayDuration = 1.5f; // 1.5-second delay between loops + static constexpr std::chrono::milliseconds delayDuration = std::chrono::milliseconds(1500); // 1.5-second delay between loops + double sampleAccumulator = 0.0; // Per-source sample accumulator for proper timing }; // OpenAL audio output device implementation @@ -299,7 +311,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { } private: - static constexpr int NUM_BUFFERS = 4; + static constexpr int NUM_BUFFERS = 8; uint32_t sampleRate = 44100; uint32_t channels = 2; @@ -370,22 +382,27 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { void ProcessAudioBuffer() { std::lock_guard lock(bufferMutex); - // Fill audio buffer from queue + // Fill audio buffer from queue in whole stereo frames to preserve channel alignment uint32_t samplesProcessed = 0; - const uint32_t maxSamples = bufferSize * channels; - - for (uint32_t i = 0; i < maxSamples && !audioQueue.empty(); i++) { + const uint32_t framesAvailable = static_cast(audioQueue.size() / channels); + if (framesAvailable == 0) { + // Not enough data for a whole frame yet + return; + } + const uint32_t framesToSend = std::min(framesAvailable, bufferSize); + const uint32_t samplesToSend = framesToSend * channels; + for (uint32_t i = 0; i < samplesToSend; i++) { audioBuffer[i] = audioQueue.front(); audioQueue.pop(); - samplesProcessed++; } + samplesProcessed = samplesToSend; if (samplesProcessed > 0) { // Convert float samples to 16-bit PCM for OpenAL std::vector pcmBuffer(samplesProcessed); for (uint32_t i = 0; i < samplesProcessed; i++) { // Clamp and convert to 16-bit PCM - float sample = std::clamp( -1.0f, 1.0f, audioBuffer[i] ); + float sample = std::clamp(audioBuffer[i], -1.0f, 1.0f); pcmBuffer[i] = static_cast(sample * 32767.0f); } @@ -479,56 +496,41 @@ AudioSystem::~AudioSystem() { void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition) { constexpr float sampleRate = 44100.0f; - const float frequency = 1000.0f; // 1000Hz ping - louder and more penetrating frequency - constexpr float pingDuration = 0.5f; // 0.5 second ping duration + const float frequency = 800.0f; // 800Hz ping to match UI label + constexpr float pingDuration = 0.75f; // 0.75 second ping duration constexpr auto pingSamples = static_cast(pingDuration * sampleRate); constexpr float silenceDuration = 1.0f; // 1 second silence after ping constexpr auto silenceSamples = static_cast(silenceDuration * sampleRate); constexpr uint32_t totalCycleSamples = pingSamples + silenceSamples; - // Debug: Track generated values - float maxGenerated = 0.0f; - uint32_t nonZeroGenerated = 0; + // Use near-constant amplitude with very short attack/release to avoid clicks and volume pumping + const uint32_t attackSamples = static_cast(0.001f * sampleRate); // ~1ms attack + const uint32_t releaseSamples = static_cast(0.001f * sampleRate); // ~1ms release + constexpr float amplitude = 0.6f; for (uint32_t i = 0; i < sampleCount; i++) { uint32_t globalPosition = playbackPosition + i; uint32_t cyclePosition = globalPosition % totalCycleSamples; if (cyclePosition < pingSamples) { - // Generate ping with envelope float t = static_cast(cyclePosition) / sampleRate; - float pingProgress = static_cast(cyclePosition) / static_cast(pingSamples); - - // Create envelope: quick attack, sustain, exponential decay - float envelope; - if (pingProgress < 0.1f) { - // Attack phase (first 10% of ping) - envelope = pingProgress / 0.1f; - } else if (pingProgress < 0.3f) { - // Sustain phase (20% of ping at full volume) - envelope = 1.0f; - } else { - // Decay phase (remaining 70% with exponential decay) - float decayProgress = (pingProgress - 0.3f) / 0.7f; - envelope = std::exp(-decayProgress * 5.0f); // Exponential decay + + // Minimal envelope for click prevention only + float envelope = 1.0f; + if (cyclePosition < attackSamples) { + envelope = static_cast(cyclePosition) / static_cast(std::max(1u, attackSamples)); + } else if (cyclePosition > pingSamples - releaseSamples) { + uint32_t relPos = pingSamples - cyclePosition; + envelope = static_cast(relPos) / static_cast(std::max(1u, releaseSamples)); } - // Generate sine wave with envelope float sineWave = sinf(2.0f * static_cast(M_PI) * frequency * t); - buffer[i] = 0.8f * envelope * sineWave; // 0.8 amplitude for much louder, clearly audible sound - - // Track generated values for debugging - float absValue = std::abs(buffer[i]); - if (absValue > 0.0001f) { - nonZeroGenerated++; - } - maxGenerated = std::max(maxGenerated, absValue); + buffer[i] = amplitude * envelope * sineWave; } else { // Silence phase buffer[i] = 0.0f; } } - } bool AudioSystem::Initialize(Engine* engine, Renderer* renderer) { @@ -580,7 +582,7 @@ bool AudioSystem::Initialize(Engine* engine, Renderer* renderer) { return true; } -void AudioSystem::Update(float deltaTime) { +void AudioSystem::Update(std::chrono::milliseconds deltaTime) { if (!initialized) { return; } @@ -613,7 +615,7 @@ void AudioSystem::Update(float deltaTime) { auto* concreteSource = dynamic_cast(source.get()); // Update playback timing and delay logic - concreteSource->UpdatePlayback(deltaTime, 0); // Will update with actual samples processed later + concreteSource->UpdatePlayback(deltaTime, 0); // Only process audio if not in the delay phase if (!concreteSource->ShouldProcessAudio()) { @@ -625,45 +627,79 @@ void AudioSystem::Update(float deltaTime) { // Get source position for spatial processing const float* sourcePosition = concreteSource->GetPosition(); - // Create sample buffers for processing - constexpr uint32_t sampleCount = 1024; - std::vector inputBuffer(sampleCount, 0.0f); - std::vector outputBuffer(sampleCount * 2, 0.0f); - uint32_t actualSamplesProcessed = 0; - - // Generate audio signal from loaded audio data - auto audioIt = audioData.find(concreteSource->GetName()); - if (audioIt != audioData.end() && !audioIt->second.empty()) { - // Use actual loaded audio data with proper position tracking - const auto& data = audioIt->second; - uint32_t playbackPos = concreteSource->GetPlaybackPosition(); - - for (uint32_t i = 0; i < sampleCount; i++) { - uint32_t dataIndex = (playbackPos + i) * 4; // 4 bytes per sample (16-bit stereo) - - if (dataIndex + 1 < data.size()) { - // Convert from 16-bit PCM to float - int16_t sample = *reinterpret_cast(&data[dataIndex]); - inputBuffer[i] = static_cast(sample) / 32768.0f; - actualSamplesProcessed++; - } else { - // Reached end of audio data - inputBuffer[i] = 0.0f; + // Accumulate samples based on real time and process in fixed-size chunks to avoid tiny buffers + double acc = concreteSource->GetSampleAccumulator(); + acc += (static_cast(deltaTime.count()) * 44100.0) / 1000.0; // ms -> samples + // Process one full ping per chunk to avoid splitting it: 0.75s at 44.1 kHz = 33075 samples + constexpr uint32_t kChunk = 33075; + uint32_t available = static_cast(acc); + if (available < kChunk) { + // Not enough for a full chunk; keep accumulating + concreteSource->SetSampleAccumulator(acc); + continue; + } + // Process as many full chunks as available this frame + while (available >= kChunk) { + std::vector inputBuffer(kChunk, 0.0f); + std::vector outputBuffer(kChunk * 2, 0.0f); + uint32_t actualSamplesProcessed = 0; + + // Generate audio signal from loaded audio data or debug ping + auto audioIt = audioData.find(concreteSource->GetName()); + if (audioIt != audioData.end() && !audioIt->second.empty()) { + // Use actual loaded audio data with proper position tracking + const auto& data = audioIt->second; + uint32_t playbackPos = concreteSource->GetPlaybackPosition(); + + for (uint32_t i = 0; i < kChunk; i++) { + uint32_t dataIndex = (playbackPos + i) * 4; // 4 bytes per sample (16-bit stereo) + + if (dataIndex + 1 < data.size()) { + // Convert from 16-bit PCM to float + int16_t sample = *reinterpret_cast(&data[dataIndex]); + inputBuffer[i] = static_cast(sample) / 32768.0f; + actualSamplesProcessed++; + } else { + // Reached end of audio data + inputBuffer[i] = 0.0f; + } } + } else { + // Generate sine wave ping for debugging + GenerateSineWavePing(inputBuffer.data(), kChunk, concreteSource->GetPlaybackPosition()); + actualSamplesProcessed = kChunk; } - } else { - // Generate sine wave ping for debugging - GenerateSineWavePing(inputBuffer.data(), sampleCount, concreteSource->GetPlaybackPosition()); - actualSamplesProcessed = sampleCount; - } - // Process audio with HRTF spatial processing using background thread - // This prevents the main thread from blocking on fence waits - // The background thread handles the complete pipeline including output to device - submitAudioTask(inputBuffer.data(), sampleCount, sourcePosition, actualSamplesProcessed); + // Build extended input [history | current] to preserve convolution continuity across chunks + uint32_t histLen = (hrtfSize > 0) ? (hrtfSize - 1) : 0; + static std::unordered_map> hrtfHistories; + auto &hist = hrtfHistories[concreteSource]; + if (hist.size() != histLen) { + hist.assign(histLen, 0.0f); + } + std::vector extendedInput(histLen + kChunk, 0.0f); + if (histLen > 0) { + std::memcpy(extendedInput.data(), hist.data(), histLen * sizeof(float)); + } + std::memcpy(extendedInput.data() + histLen, inputBuffer.data(), kChunk * sizeof(float)); + + // Submit for GPU HRTF processing via the background thread (trim will occur in processAudioTask) + submitAudioTask(extendedInput.data(), static_cast(extendedInput.size()), sourcePosition, actualSamplesProcessed, histLen); + + // Update history with the tail of current input + if (histLen > 0) { + std::memcpy(hist.data(), inputBuffer.data() + (kChunk - histLen), histLen * sizeof(float)); + } + + // Update playback timing with actual samples processed + concreteSource->UpdatePlayback(std::chrono::milliseconds(0), actualSamplesProcessed); - // Update playback timing with actual samples processed - concreteSource->UpdatePlayback(0.0f, actualSamplesProcessed); + // Consume one chunk from the accumulator + acc -= static_cast(kChunk); + available -= kChunk; + } + // Store fractional remainder for next frame + concreteSource->SetSampleAccumulator(acc); } } @@ -684,15 +720,15 @@ void AudioSystem::Update(float deltaTime) { }); // Update timing for audio processing with low-latency chunks - static float accumulatedTime = 0.0f; + static std::chrono::milliseconds accumulatedTime = std::chrono::milliseconds(0); accumulatedTime += deltaTime; // Process audio in 20ms chunks for optimal latency - constexpr float audioChunkTime = 0.02f; // 20ms chunks for real-time audio + constexpr std::chrono::milliseconds audioChunkTime = std::chrono::milliseconds(20); // 20ms chunks for real-time audio if (accumulatedTime >= audioChunkTime) { // Trigger audio buffer updates for smooth playback // The HRTF processing ensures spatial audio is updated continuously - accumulatedTime = 0.0f; + accumulatedTime = std::chrono::milliseconds(0); // Update listener properties if they have changed // This ensures spatial audio positioning stays current with camera movement @@ -796,14 +832,15 @@ AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { auto source = std::make_unique(name); // Set up debug ping parameters - // The ping will cycle every 1.5 seconds (0.5 s ping + 1.0 s silence) + // The ping will cycle every 1.5 seconds (0.5s ping + 1.0s silence) constexpr float sampleRate = 44100.0f; constexpr float pingDuration = 0.5f; constexpr float silenceDuration = 1.0f; constexpr auto totalCycleSamples = static_cast((pingDuration + silenceDuration) * sampleRate); - // Set the audio length for proper timing (infinite loop for debugging) - source->SetAudioLength(totalCycleSamples); + // For generated ping, let the generator control the 0.5s ping + 1.0s silence cycle. + // Disable source-level length/delay to avoid double-silence and audible resets. + source->SetAudioLength(0); // Store the source sources.push_back(std::move(source)); @@ -846,7 +883,9 @@ bool AudioSystem::IsHRTFEnabled() const { } void AudioSystem::SetHRTFCPUOnly(const bool cpuOnly) { - hrtfCPUOnly = cpuOnly; + (void)cpuOnly; + // Enforce GPU-only HRTF processing: ignore CPU-only requests + hrtfCPUOnly = false; } bool AudioSystem::IsHRTFCPUOnly() const { @@ -859,7 +898,9 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { constexpr uint32_t hrtfSampleCount = 256; // Number of samples per impulse response constexpr uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) constexpr uint32_t channelCount = 2; // Stereo (left and right ears) - // const float headRadius = 0.0875f; // Average head radius in meters + const float sampleRate = 44100.0f; // Sample rate for HRTF data + const float speedOfSound = 343.0f; // Speed of sound in m/s + const float headRadius = 0.0875f; // Average head radius in meters // Try to load from a file first (only if the filename is provided) if (!filename.empty()) { @@ -909,8 +950,6 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { float z = std::cos(elevation) * std::cos(azimuth); for (uint32_t channel = 0; channel < channelCount; channel++) { - constexpr float speedOfSound = 343.0f; - constexpr float sampleRate = 44100.0f; // Calculate ear position (left ear: -0.1m, right ear: +0.1m on x-axis) float earX = (channel == 0) ? -0.1f : 0.1f; @@ -934,6 +973,7 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { // Generate impulse response + uint32_t samplesGenerated = 0; for (uint32_t i = 0; i < hrtfSampleCount; i++) { float value = 0.0f; @@ -943,15 +983,6 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { value = shadowFactor * std::exp(-t * 1000.0f) * std::cos(2.0f * static_cast(M_PI) * 1000.0f * t); } - // Early reflections (simplified) - for (int refl = 1; refl <= 3; refl++) { - uint32_t reflDelay = sampleDelay + refl * 20; - if (i >= reflDelay && i < reflDelay + 5) { - float t = static_cast(i - reflDelay) / sampleRate; - float reflGain = shadowFactor * 0.3f / static_cast(refl); - value += reflGain * std::exp(-t * 2000.0f) * std::cos(2.0f * static_cast(M_PI) * 800.0f * t); - } - } // Apply distance attenuation value /= std::max(1.0f, distance); @@ -1050,19 +1081,39 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin int hrtfIndex = elevationIndex * 36 + azimuthIndex; hrtfIndex = std::min(hrtfIndex, static_cast(numHrtfPositions) - 1); - // Perform convolution for left and right ears + // Perform convolution for left and right ears with simple overlap-add using per-direction input history + static std::unordered_map> convHistories; // mono histories keyed by hrtfIndex + const uint32_t histLenDesired = (hrtfSize > 0) ? (hrtfSize - 1) : 0; + auto &convHistory = convHistories[hrtfIndex]; + if (convHistory.size() != histLenDesired) { + convHistory.assign(histLenDesired, 0.0f); + } + + // Build extended input: [history | current input] + std::vector extInput(histLenDesired + sampleCount, 0.0f); + if (histLenDesired > 0) { + std::memcpy(extInput.data(), convHistory.data(), histLenDesired * sizeof(float)); + } + if (sampleCount > 0) { + std::memcpy(extInput.data() + histLenDesired, inputBuffer, sampleCount * sizeof(float)); + } + for (uint32_t i = 0; i < sampleCount; i++) { float leftSample = 0.0f; float rightSample = 0.0f; - // Convolve with HRTF impulse response - for (uint32_t j = 0; j < hrtfSize && j <= i; j++) { + // Convolve with HRTF impulse response using extended input + // extIndex = histLenDesired + i - j; ensure extIndex >= 0 + uint32_t jMax = std::min(hrtfSize - 1, histLenDesired + i); + for (uint32_t j = 0; j <= jMax; j++) { + uint32_t extIndex = histLenDesired + i - j; uint32_t hrtfLeftIndex = hrtfIndex * hrtfSize * 2 + j; uint32_t hrtfRightIndex = hrtfIndex * hrtfSize * 2 + hrtfSize + j; if (hrtfLeftIndex < hrtfData.size() && hrtfRightIndex < hrtfData.size()) { - leftSample += inputBuffer[i - j] * hrtfData[hrtfLeftIndex]; - rightSample += inputBuffer[i - j] * hrtfData[hrtfRightIndex]; + float in = extInput[extIndex]; + leftSample += in * hrtfData[hrtfLeftIndex]; + rightSample += in * hrtfData[hrtfRightIndex]; } } @@ -1076,6 +1127,11 @@ bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uin outputBuffer[i * 2 + 1] = rightSample; } + // Update history with the tail of the extended input + if (histLenDesired > 0) { + std::memcpy(convHistory.data(), extInput.data() + sampleCount, histLenDesired * sizeof(float)); + } + return true; @@ -1412,20 +1468,30 @@ void AudioSystem::processAudioTask(const std::shared_ptr& task) { task->sampleCount, task->sourcePosition); if (success && task->outputDevice && task->outputDevice->IsPlaying()) { - // Apply master volume in the background thread - for (uint32_t i = 0; i < task->sampleCount * 2; i++) { - task->outputBuffer[i] *= task->masterVolume; + // We used extended input of length sampleCount = histLen + outFrames. + // Trim the first trimFront frames from the stereo output and only write actualSamplesProcessed frames. + uint32_t startFrame = task->trimFront; + uint32_t framesToWrite = task->actualSamplesProcessed; + if (startFrame * 2 > task->outputBuffer.size()) { + startFrame = 0; // safety + } + if (startFrame * 2 + framesToWrite * 2 > task->outputBuffer.size()) { + framesToWrite = static_cast((task->outputBuffer.size() / 2) - startFrame); + } + float* startPtr = task->outputBuffer.data() + startFrame * 2; + // Apply master volume only to the range we will write + for (uint32_t i = 0; i < framesToWrite * 2; i++) { + startPtr[i] *= task->masterVolume; } - // Send processed audio directly to output device from background thread - if (!task->outputDevice->WriteAudio(task->outputBuffer.data(), task->sampleCount)) { + if (!task->outputDevice->WriteAudio(startPtr, framesToWrite)) { std::cerr << "Failed to write audio data to output device from background thread" << std::endl; } } } bool AudioSystem::submitAudioTask(const float* inputBuffer, uint32_t sampleCount, - const float* sourcePosition, uint32_t actualSamplesProcessed) { + const float* sourcePosition, uint32_t actualSamplesProcessed, uint32_t trimFront) { if (!audioThreadRunning.load()) { // Fallback to synchronous processing if the thread is not running std::vector outputBuffer(sampleCount * 2); @@ -1451,8 +1517,9 @@ bool AudioSystem::submitAudioTask(const float* inputBuffer, uint32_t sampleCount task->inputBuffer.assign(inputBuffer, inputBuffer + sampleCount); task->outputBuffer.resize(sampleCount * 2); // Stereo output memcpy(task->sourcePosition, sourcePosition, sizeof(float) * 3); - task->sampleCount = sampleCount; - task->actualSamplesProcessed = actualSamplesProcessed; + task->sampleCount = sampleCount; // includes history frames + task->actualSamplesProcessed = actualSamplesProcessed; // new frames only (kChunk) + task->trimFront = sampleCount - actualSamplesProcessed; // history length (histLen) task->outputDevice = outputDevice.get(); task->masterVolume = masterVolume; @@ -1465,3 +1532,26 @@ bool AudioSystem::submitAudioTask(const float* inputBuffer, uint32_t sampleCount return true; // Return immediately without waiting } + + + +void AudioSystem::FlushOutput() { + // Stop background processing to avoid races while flushing + stopAudioThread(); + + // Clear any pending audio processing tasks + { + std::lock_guard lock(taskQueueMutex); + std::queue> empty; + std::swap(audioTaskQueue, empty); + } + + // Flush the output device buffers and queues by restart + if (outputDevice) { + outputDevice->Stop(); + outputDevice->Start(); + } + + // Restart background processing + startAudioThread(); +} diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index f3193396..4e5037c8 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -148,6 +148,11 @@ class AudioSystem { */ AudioSystem() = default; + /** + * @brief Flush audio output: clears pending processing and device buffers so playback restarts cleanly. + */ + void FlushOutput(); + /** * @brief Destructor for proper cleanup. */ @@ -165,7 +170,7 @@ class AudioSystem { * @brief Update the audio system. * @param deltaTime The time elapsed since the last update. */ - void Update(float deltaTime); + void Update(std::chrono::milliseconds deltaTime); /** * @brief Load an audio file. @@ -318,8 +323,9 @@ class AudioSystem { std::vector inputBuffer; std::vector outputBuffer; float sourcePosition[3]; - uint32_t sampleCount; - uint32_t actualSamplesProcessed; + uint32_t sampleCount; // total frames in input/output (may include history) + uint32_t actualSamplesProcessed; // frames to write this tick (new part) + uint32_t trimFront; // frames to skip from output front (history length) AudioOutputDevice* outputDevice; float masterVolume; }; @@ -392,5 +398,5 @@ class AudioSystem { * @param actualSamplesProcessed The number of samples actually processed. * @return True if the task was submitted successfully, false otherwise. */ - bool submitAudioTask(const float* inputBuffer, uint32_t sampleCount, const float* sourcePosition, uint32_t actualSamplesProcessed); + bool submitAudioTask(const float* inputBuffer, uint32_t sampleCount, const float* sourcePosition, uint32_t actualSamplesProcessed, uint32_t trimFront); }; diff --git a/attachments/simple_engine/component.cpp b/attachments/simple_engine/component.cpp index 157ade0b..f2cb04ba 100644 --- a/attachments/simple_engine/component.cpp +++ b/attachments/simple_engine/component.cpp @@ -1,5 +1,4 @@ #include "component.h" -#include "entity.h" // Most of the Component class implementation is in the header file // This file is mainly for any methods that need to access the Entity class diff --git a/attachments/simple_engine/component.h b/attachments/simple_engine/component.h index 85327d32..65480c71 100644 --- a/attachments/simple_engine/component.h +++ b/attachments/simple_engine/component.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include // Forward declaration @@ -44,7 +44,7 @@ class Component { * Called every frame. * @param deltaTime The time elapsed since the last frame. */ - virtual void Update(float deltaTime) {} + virtual void Update(std::chrono::milliseconds deltaTime) {} /** * @brief Render the component. diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 9c9a531e..4fc3fb91 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -122,11 +123,11 @@ void Engine::Run() { } // Calculate delta time - deltaTime = CalculateDeltaTime(); + deltaTimeMs = CalculateDeltaTimeMs(); // Update frame counter and FPS frameCount++; - fpsUpdateTimer += deltaTime; + fpsUpdateTimer += deltaTimeMs.count() * 0.001f; // Update window title with FPS and frame time every second if (fpsUpdateTimer >= 1.0f) { @@ -147,7 +148,7 @@ void Engine::Run() { } // Update - Update(deltaTime); + Update(deltaTimeMs); // Render Render(); @@ -369,7 +370,7 @@ void Engine::handleKeyInput(uint32_t key, bool pressed) { } } -void Engine::Update(float deltaTime) { +void Engine::Update(TimeDelta deltaTime) { // Debug: Verify Update method is being called static int updateCallCount = 0; updateCallCount++; @@ -415,7 +416,7 @@ void Engine::Render() { renderer->Render(entities, activeCamera, imguiSystem.get()); } -float Engine::CalculateDeltaTime() { +std::chrono::milliseconds Engine::CalculateDeltaTimeMs() { // Get current time using a steady clock to avoid system time jumps uint64_t currentTime = static_cast( std::chrono::duration_cast( @@ -423,19 +424,19 @@ float Engine::CalculateDeltaTime() { ).count() ); - // Initialize lastFrameTime on first call - if (lastFrameTime == 0) { - lastFrameTime = currentTime; - return 0.016f; // ~16ms as a sane initial guess + // Initialize lastFrameTimeMs on first call + if (lastFrameTimeMs == 0) { + lastFrameTimeMs = currentTime; + return std::chrono::milliseconds(16); // ~16ms as a sane initial guess } // Calculate delta time in milliseconds - uint64_t delta = currentTime - lastFrameTime; + uint64_t delta = currentTime - lastFrameTimeMs; // Update last frame time - lastFrameTime = currentTime; + lastFrameTimeMs = currentTime; - return static_cast(delta) / 1000.0f; + return std::chrono::milliseconds(static_cast(delta)); } void Engine::HandleResize(int width, int height) const { @@ -458,7 +459,7 @@ void Engine::HandleResize(int width, int height) const { } } -void Engine::UpdateCameraControls(float deltaTime) const { +void Engine::UpdateCameraControls(TimeDelta deltaTime) const { if (!activeCamera) return; // Get a camera transform component @@ -492,7 +493,7 @@ void Engine::UpdateCameraControls(float deltaTime) const { // Manual camera controls (only when tracking is disabled) // Calculate movement speed - float velocity = cameraControl.cameraSpeed * deltaTime; + float velocity = cameraControl.cameraSpeed * deltaTime.count() * .001f; // Calculate camera direction vectors based on yaw and pitch glm::vec3 front; @@ -859,10 +860,10 @@ void Engine::RunAndroid() { // We just need to update and render when the platform is ready // Calculate delta time - deltaTime = CalculateDeltaTime(); + deltaTimeMs = CalculateDeltaTimeMs(); // Update - Update(deltaTime); + Update(deltaTimeMs); // Render Render(); diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index dcb68a25..446f41fb 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "platform.h" #include "renderer.h" @@ -23,6 +24,7 @@ */ class Engine { public: + using TimeDelta = std::chrono::milliseconds; /** * @brief Default constructor. */ @@ -197,8 +199,9 @@ class Engine { bool running = false; // Delta time calculation - float deltaTime = 0.0f; - uint64_t lastFrameTime = 0; + // deltaTimeMs: time since last frame in milliseconds (for clarity) + std::chrono::milliseconds deltaTimeMs{0}; + uint64_t lastFrameTimeMs = 0; // Frame counter and FPS calculation uint64_t frameCount = 0; @@ -269,7 +272,8 @@ class Engine { * @brief Update the engine state. * @param deltaTime The time elapsed since the last update. */ - void Update(float deltaTime); + // Accepts a time delta in milliseconds for clarity + void Update(TimeDelta deltaTime); /** * @brief Render the scene. @@ -277,10 +281,10 @@ class Engine { void Render(); /** - * @brief Calculate the delta time between frames. - * @return The delta time in seconds. + * @brief Calculate the time delta between frames. + * @return The delta time in milliseconds (steady_clock based). */ - float CalculateDeltaTime(); + std::chrono::milliseconds CalculateDeltaTimeMs(); /** * @brief Handle window resize events. @@ -293,7 +297,7 @@ class Engine { * @brief Update camera controls based on input state. * @param deltaTime The time elapsed since the last update. */ - void UpdateCameraControls(float deltaTime) const; + void UpdateCameraControls(TimeDelta deltaTime) const; /** * @brief Generate random PBR material properties for the ball. diff --git a/attachments/simple_engine/entity.cpp b/attachments/simple_engine/entity.cpp index f9dd4860..41292561 100644 --- a/attachments/simple_engine/entity.cpp +++ b/attachments/simple_engine/entity.cpp @@ -9,7 +9,7 @@ void Entity::Initialize() { } } -void Entity::Update(float deltaTime) { +void Entity::Update(std::chrono::milliseconds deltaTime) { if (!active) return; for (auto& component : components) { diff --git a/attachments/simple_engine/entity.h b/attachments/simple_engine/entity.h index 9be71f3b..75de36c6 100644 --- a/attachments/simple_engine/entity.h +++ b/attachments/simple_engine/entity.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -62,7 +63,7 @@ class Entity { * @brief Update all components of the entity. * @param deltaTime The time elapsed since the last frame. */ - void Update(float deltaTime); + void Update(std::chrono::milliseconds deltaTime); /** * @brief Render all components of the entity. diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 5c2d21ff..ecab5304 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -281,6 +281,7 @@ void ImGuiSystem::NewFrame() { if (ImGui::Button("Play")) { if (currentSource) { currentSource->Play(); + if (audioSystem) { audioSystem->FlushOutput(); } if (useDebugPing) { std::cout << "Started playing debug ping (800Hz sine wave) with HRTF processing" << std::endl; } else { @@ -311,34 +312,10 @@ void ImGuiSystem::NewFrame() { ImGui::Text("Use directional buttons to move the audio source in 3D space"); ImGui::Text("You should hear the audio move around you!"); - // HRTF Processing Mode Selection + // HRTF Processing Mode: GPU only (checkbox removed) ImGui::Separator(); ImGui::Text("HRTF Processing Mode:"); - - static bool cpuOnlyMode = false; - static bool initialized = false; - - // Initialize checkbox state to match audio system state - if (!initialized && audioSystem) { - cpuOnlyMode = audioSystem->IsHRTFCPUOnly(); - initialized = true; - } - - if (ImGui::Checkbox("Force CPU-only HRTF processing", &cpuOnlyMode)) { - if (audioSystem) { - audioSystem->SetHRTFCPUOnly(cpuOnlyMode); - std::cout << "HRTF processing mode changed to: " << (cpuOnlyMode ? "CPU-only" : "Vulkan shader (when available)") << std::endl; - } - } - - // Display current processing mode - if (audioSystem) { - if (audioSystem->IsHRTFCPUOnly()) { - ImGui::Text("Current Mode: CPU-only processing"); - } else { - ImGui::Text("Current Mode: Vulkan shader processing (when available)"); - } - } + ImGui::Text("Current Mode: Vulkan shader processing (GPU)"); } else { ImGui::Text("HRTF Processing: DISABLED"); } diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index d84c5e09..fc2adf7b 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -201,7 +201,7 @@ bool PhysicsSystem::Initialize() { return true; } -void PhysicsSystem::Update(float deltaTime) { +void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) { // GPU-ONLY physics - NO CPU fallback available // Check if GPU physics is properly initialized and available @@ -977,7 +977,7 @@ void PhysicsSystem::CleanupVulkanResources() { vulkanResources.physicsBufferMemory = nullptr; } -void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { +void PhysicsSystem::UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) const { if (!renderer) { return; } @@ -1082,7 +1082,7 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { // Update params buffer PhysicsParams params{}; - params.deltaTime = deltaTime; // Use actual deltaTime instead of fixed timestep + params.deltaTime = deltaTime.count() * 0.001f; // Use actual deltaTime instead of fixed timestep params.numBodies = static_cast(rigidBodies.size()); params.maxCollisions = maxGPUCollisions; params.padding = 0.0f; // Initialize padding to zero for proper std140 alignment @@ -1192,7 +1192,7 @@ void PhysicsSystem::ReadbackGPUPhysicsData() const { } } -void PhysicsSystem::SimulatePhysicsOnGPU(const float deltaTime) const { +void PhysicsSystem::SimulatePhysicsOnGPU(const std::chrono::milliseconds deltaTime) const { if (!renderer) { fprintf(stderr, "SimulatePhysicsOnGPU: No renderer available"); return; diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index 4c264799..ed4c1d3b 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -209,7 +210,7 @@ class PhysicsSystem { * @brief Update the physics system. * @param deltaTime The time elapsed since the last update. */ - void Update(float deltaTime); + void Update(std::chrono::milliseconds deltaTime); /** * @brief Create a rigid body. @@ -361,11 +362,11 @@ class PhysicsSystem { void CleanupVulkanResources(); // Update physics data on the GPU - void UpdateGPUPhysicsData(float deltaTime) const; + void UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) const; // Read back physics data from the GPU void ReadbackGPUPhysicsData() const; // Perform GPU-accelerated physics simulation - void SimulatePhysicsOnGPU(float deltaTime) const; + void SimulatePhysicsOnGPU(std::chrono::milliseconds deltaTime) const; }; From aa321c04e21606ea024962982dacd627ce063621 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 18 Aug 2025 19:36:24 -0700 Subject: [PATCH 090/102] addressing more comments. --- attachments/simple_engine/audio_system.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 6d90bd24..673b03dd 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -496,14 +496,13 @@ AudioSystem::~AudioSystem() { void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition) { constexpr float sampleRate = 44100.0f; - const float frequency = 800.0f; // 800Hz ping to match UI label + const float frequency = 800.0f; // 800Hz ping constexpr float pingDuration = 0.75f; // 0.75 second ping duration constexpr auto pingSamples = static_cast(pingDuration * sampleRate); constexpr float silenceDuration = 1.0f; // 1 second silence after ping constexpr auto silenceSamples = static_cast(silenceDuration * sampleRate); constexpr uint32_t totalCycleSamples = pingSamples + silenceSamples; - // Use near-constant amplitude with very short attack/release to avoid clicks and volume pumping const uint32_t attackSamples = static_cast(0.001f * sampleRate); // ~1ms attack const uint32_t releaseSamples = static_cast(0.001f * sampleRate); // ~1ms release constexpr float amplitude = 0.6f; @@ -630,7 +629,6 @@ void AudioSystem::Update(std::chrono::milliseconds deltaTime) { // Accumulate samples based on real time and process in fixed-size chunks to avoid tiny buffers double acc = concreteSource->GetSampleAccumulator(); acc += (static_cast(deltaTime.count()) * 44100.0) / 1000.0; // ms -> samples - // Process one full ping per chunk to avoid splitting it: 0.75s at 44.1 kHz = 33075 samples constexpr uint32_t kChunk = 33075; uint32_t available = static_cast(acc); if (available < kChunk) { From 5c4ec279922faac0b7181c95779fb1b18e291f46 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 18 Aug 2025 19:52:19 -0700 Subject: [PATCH 091/102] addressing more comments. --- attachments/simple_engine/entity.h | 37 ++++++++---------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/attachments/simple_engine/entity.h b/attachments/simple_engine/entity.h index 75de36c6..0ef252f2 100644 --- a/attachments/simple_engine/entity.h +++ b/attachments/simple_engine/entity.h @@ -5,8 +5,6 @@ #include #include #include -#include -#include #include #include "component.h" @@ -22,7 +20,6 @@ class Entity { std::string name; bool active = true; std::vector> components; - std::unordered_map componentMap; public: /** @@ -88,10 +85,7 @@ class Entity { // Set the owner componentPtr->SetOwner(this); - // Add to the map for quick lookup - componentMap[std::type_index(typeid(T))] = componentPtr; - - // Add to the vector for ownership + // Add to the vector for ownership and iteration components.push_back(std::move(component)); // Initialize the component @@ -109,11 +103,12 @@ class Entity { T* GetComponent() const { static_assert(std::is_base_of::value, "T must derive from Component"); - auto it = componentMap.find(std::type_index(typeid(T))); - if (it != componentMap.end()) { - return static_cast(it->second); + // Search from the back to preserve previous behavior of returning the last-added component of type T + for (auto it = components.rbegin(); it != components.rend(); ++it) { + if (auto* casted = dynamic_cast(it->get())) { + return casted; + } } - return nullptr; } @@ -126,21 +121,9 @@ class Entity { bool RemoveComponent() { static_assert(std::is_base_of::value, "T must derive from Component"); - auto it = componentMap.find(std::type_index(typeid(T))); - if (it != componentMap.end()) { - Component* componentPtr = it->second; - - // Remove from the map - componentMap.erase(it); - - // Find and remove from the vector - auto vecIt = std::find_if(components.begin(), components.end(), - [componentPtr](const std::unique_ptr& comp) { - return comp.get() == componentPtr; - }); - - if (vecIt != components.end()) { - components.erase(vecIt); + for (auto it = components.rbegin(); it != components.rend(); ++it) { + if (dynamic_cast(it->get()) != nullptr) { + components.erase(std::next(it).base()); return true; } } @@ -156,6 +139,6 @@ class Entity { template bool HasComponent() const { static_assert(std::is_base_of::value, "T must derive from Component"); - return componentMap.contains(std::type_index(typeid(T))); + return GetComponent() != nullptr; } }; From b4d62268316eb5c6f32ea4eee8c48d30c0ebed6a Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 18 Aug 2025 19:58:56 -0700 Subject: [PATCH 092/102] addressing more comments. --- attachments/simple_engine/engine.h | 4 ++++ attachments/simple_engine/main.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 446f41fb..e7b570d7 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -169,7 +169,11 @@ class Engine { * @param enableValidationLayers Whether to enable Vulkan validation layers. * @return True if initialization was successful, false otherwise. */ + #if defined(NDEBUG) + bool InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers = false); + #else bool InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers = true); + #endif /** * @brief Run the engine on Android. diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index 4225249f..5afeba11 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -9,7 +9,11 @@ // Constants constexpr int WINDOW_WIDTH = 800; constexpr int WINDOW_HEIGHT = 600; +#if defined(NDEBUG) +constexpr bool ENABLE_VALIDATION_LAYERS = false; +#else constexpr bool ENABLE_VALIDATION_LAYERS = true; +#endif /** From a8d11c6035f07ef7de0c28d091dfbf2c7e55d194 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 19 Aug 2025 04:22:43 -0700 Subject: [PATCH 093/102] addressing more comments. --- attachments/simple_engine/engine.h | 2 +- attachments/simple_engine/memory_pool.cpp | 105 ++++++------------ attachments/simple_engine/memory_pool.h | 16 ++- attachments/simple_engine/renderer_core.cpp | 2 +- .../simple_engine/renderer_rendering.cpp | 2 +- .../simple_engine/renderer_resources.cpp | 6 - 6 files changed, 42 insertions(+), 91 deletions(-) diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index e7b570d7..0e142407 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -261,7 +261,7 @@ class Engine { PhysicsScaling physicsScaling; - // Pending ball creation data (to avoid memory pool constraints during rendering) + // Pending ball creation data struct PendingBall { glm::vec3 spawnPosition; glm::vec3 throwDirection; diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp index 13d8ec64..a150592f 100644 --- a/attachments/simple_engine/memory_pool.cpp +++ b/attachments/simple_engine/memory_pool.cpp @@ -23,8 +23,7 @@ bool MemoryPool::initialize() { PoolType::VERTEX_BUFFER, 128 * 1024 * 1024, // 128MB blocks (doubled) 4096, // 4KB allocation units - vk::MemoryPropertyFlagBits::eDeviceLocal, - 16 // Max 16 blocks (2GB total, quadrupled) + vk::MemoryPropertyFlagBits::eDeviceLocal ); // Index buffer pool: Medium allocations, device-local (increased for large models like bistro) @@ -32,8 +31,7 @@ bool MemoryPool::initialize() { PoolType::INDEX_BUFFER, 64 * 1024 * 1024, // 64MB blocks (doubled) 2048, // 2KB allocation units - vk::MemoryPropertyFlagBits::eDeviceLocal, - 8 // Max 8 blocks (512MB total, quadrupled) + vk::MemoryPropertyFlagBits::eDeviceLocal ); // Uniform buffer pool: Small allocations, host-visible @@ -42,8 +40,7 @@ bool MemoryPool::initialize() { PoolType::UNIFORM_BUFFER, 4 * 1024 * 1024, // 4MB blocks 64, // 64B allocation units (aligned to nonCoherentAtomSize) - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, - 4 // Max 4 blocks (16MB total) + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); // Staging buffer pool: Variable allocations, host-visible @@ -52,8 +49,7 @@ bool MemoryPool::initialize() { PoolType::STAGING_BUFFER, 16 * 1024 * 1024, // 16MB blocks 64, // 64B allocation units (aligned to nonCoherentAtomSize) - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, - 4 // Max 4 blocks (64MB total) + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); // Texture image pool: Large allocations, device-local (significantly increased for large models like bistro) @@ -61,8 +57,7 @@ bool MemoryPool::initialize() { PoolType::TEXTURE_IMAGE, 256 * 1024 * 1024, // 256MB blocks (doubled) 4096, // 4KB allocation units - vk::MemoryPropertyFlagBits::eDeviceLocal, - 24 // Max 24 blocks (6GB total, 6x increase) + vk::MemoryPropertyFlagBits::eDeviceLocal ); return true; @@ -76,14 +71,12 @@ void MemoryPool::configurePool( const PoolType poolType, const vk::DeviceSize blockSize, const vk::DeviceSize allocationUnit, - const vk::MemoryPropertyFlags properties, - const uint32_t maxBlocks) { + const vk::MemoryPropertyFlags properties) { PoolConfig config; config.blockSize = blockSize; config.allocationUnit = allocationUnit; config.properties = properties; - config.maxBlocks = maxBlocks; poolConfigs[poolType] = config; } @@ -126,16 +119,16 @@ std::unique_ptr MemoryPool::createMemoryBlock(PoolType uint32_t memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, config.properties); - // Allocate the memory block + // Allocate the memory block using the device-required size vk::MemoryAllocateInfo allocInfo{ - .allocationSize = blockSize, + .allocationSize = memRequirements.size, .memoryTypeIndex = memoryTypeIndex }; // Create MemoryBlock with proper initialization to avoid default constructor issues auto block = std::unique_ptr(new MemoryBlock{ .memory = vk::raii::DeviceMemory(device, allocInfo), - .size = blockSize, + .size = memRequirements.size, .used = 0, .memoryTypeIndex = memoryTypeIndex, .isMapped = false, @@ -147,24 +140,23 @@ std::unique_ptr MemoryPool::createMemoryBlock(PoolType // Map memory if it's host-visible block->isMapped = (config.properties & vk::MemoryPropertyFlagBits::eHostVisible) != vk::MemoryPropertyFlags{}; if (block->isMapped) { - block->mappedPtr = block->memory.mapMemory(0, blockSize); + block->mappedPtr = block->memory.mapMemory(0, memRequirements.size); } else { block->mappedPtr = nullptr; } - // Initialize a free list - const size_t numUnits = blockSize / config.allocationUnit; + // Initialize a free list based on the actual allocated size + const size_t numUnits = static_cast(block->size / config.allocationUnit); block->freeList.resize(numUnits, true); // All units initially free return block; } -MemoryPool::MemoryBlock* MemoryPool::findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { +std::pair MemoryPool::findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { auto poolIt = pools.find(poolType); if (poolIt == pools.end()) { - pools[poolType] = std::vector>(); - poolIt = pools.find(poolType); + poolIt = pools.try_emplace( poolType ).first; } auto& poolBlocks = poolIt->second; @@ -178,11 +170,15 @@ MemoryPool::MemoryBlock* MemoryPool::findSuitableBlock(PoolType poolType, vk::De for (const auto& block : poolBlocks) { // Find consecutive free units size_t consecutiveFree = 0; + size_t startUnitCandidate = 0; for (size_t i = 0; i < block->freeList.size(); ++i) { if (block->freeList[i]) { + if (consecutiveFree == 0) { + startUnitCandidate = i; + } consecutiveFree++; if (consecutiveFree >= requiredUnits) { - return block.get(); + return {block.get(), startUnitCandidate}; } } else { consecutiveFree = 0; @@ -190,35 +186,23 @@ MemoryPool::MemoryBlock* MemoryPool::findSuitableBlock(PoolType poolType, vk::De } } - // No suitable block found, create a new one if we haven't reached the limit AND rendering is not active - if (poolBlocks.size() < config.maxBlocks) { - if (renderingActive) { - std::cerr << "ERROR: Attempted to create new memory block during rendering! Pool type: " - << static_cast(poolType) << ", required size: " << alignedSize << std::endl; - std::cerr << "This violates the constraint that no new memory should be allocated during PBR rendering." << std::endl; - return nullptr; - } - - try { - auto newBlock = createMemoryBlock(poolType, alignedSize); - poolBlocks.push_back(std::move(newBlock)); - std::cout << "Created new memory block during initialization (pool type: " - << static_cast(poolType) << ")" << std::endl; - return poolBlocks.back().get(); - } catch (const std::exception& e) { - std::cerr << "Failed to create new memory block: " << e.what() << std::endl; - return nullptr; - } + // No suitable block found; create a new one on demand (no hard limits, allowed during rendering) + try { + auto newBlock = createMemoryBlock(poolType, alignedSize); + poolBlocks.push_back(std::move(newBlock)); + std::cout << "Created new memory block (pool type: " + << static_cast(poolType) << ")" << std::endl; + return {poolBlocks.back().get(), 0}; + } catch (const std::exception& e) { + std::cerr << "Failed to create new memory block: " << e.what() << std::endl; + return {nullptr, 0}; } - - std::cerr << "Memory pool exhausted for pool type " << static_cast(poolType) << std::endl; - return nullptr; } std::unique_ptr MemoryPool::allocate(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { std::lock_guard lock(poolMutex); - MemoryBlock* block = findSuitableBlock(poolType, size, alignment); + auto [block, startUnit] = findSuitableBlock(poolType, size, alignment); if (!block) { return nullptr; } @@ -229,30 +213,6 @@ std::unique_ptr MemoryPool::allocate(PoolType poolType, const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; const size_t requiredUnits = (alignedSize + config.allocationUnit - 1) / config.allocationUnit; - // Find consecutive free units - size_t startUnit = 0; - size_t consecutiveFree = 0; - bool found = false; - - for (size_t i = 0; i < block->freeList.size(); ++i) { - if (block->freeList[i]) { - if (consecutiveFree == 0) { - startUnit = i; - } - consecutiveFree++; - if (consecutiveFree >= requiredUnits) { - found = true; - break; - } - } else { - consecutiveFree = 0; - } - } - - if (!found) { - return nullptr; - } - // Mark units as used for (size_t i = startUnit; i < startUnit + requiredUnits; ++i) { block->freeList[i] = false; @@ -427,14 +387,13 @@ bool MemoryPool::preAllocatePools() { std::lock_guard lock(poolMutex); try { - std::cout << "Pre-allocating memory pools to prevent allocation during rendering..." << std::endl; + std::cout << "Pre-allocating initial memory blocks for pools..." << std::endl; // Pre-allocate at least one block for each pool type for (const auto& [poolType, config] : poolConfigs) { auto poolIt = pools.find(poolType); if (poolIt == pools.end()) { - pools[poolType] = std::vector>(); - poolIt = pools.find(poolType); + poolIt = pools.try_emplace( poolType ).first; } auto& poolBlocks = poolIt->second; diff --git a/attachments/simple_engine/memory_pool.h b/attachments/simple_engine/memory_pool.h index 1bea7fef..49dc7641 100644 --- a/attachments/simple_engine/memory_pool.h +++ b/attachments/simple_engine/memory_pool.h @@ -6,6 +6,7 @@ #include #include #include +#include /** * @brief Memory pool allocator for Vulkan resources @@ -62,7 +63,6 @@ class MemoryPool { vk::DeviceSize blockSize; // Size of each memory block vk::DeviceSize allocationUnit; // Minimum allocation unit vk::MemoryPropertyFlags properties; // Memory properties - uint32_t maxBlocks; // Maximum number of blocks }; // Memory pools for different types @@ -72,13 +72,13 @@ class MemoryPool { // Thread safety mutable std::mutex poolMutex; - // Rendering state tracking to prevent pool growth during rendering + // Optional rendering state flag (no allocation restrictions enforced) bool renderingActive = false; // Helper methods uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; std::unique_ptr createMemoryBlock(PoolType poolType, vk::DeviceSize size); - MemoryBlock* findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment); + std::pair findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment); public: /** @@ -165,30 +165,28 @@ class MemoryPool { * @param blockSize Size of each memory block * @param allocationUnit Minimum allocation unit * @param properties Memory properties - * @param maxBlocks Maximum number of blocks */ void configurePool( PoolType poolType, vk::DeviceSize blockSize, vk::DeviceSize allocationUnit, - vk::MemoryPropertyFlags properties, - uint32_t maxBlocks + vk::MemoryPropertyFlags properties ); /** - * @brief Pre-allocate memory pools to prevent allocation during rendering + * @brief Pre-allocate initial memory blocks for configured pools * @return True if pre-allocation was successful */ bool preAllocatePools(); /** - * @brief Set rendering active state to prevent pool growth + * @brief Set rendering active state flag (informational only) * @param active Whether rendering is currently active */ void setRenderingActive(bool active); /** - * @brief Check if rendering is currently active + * @brief Check if rendering is currently active (informational only) * @return True if rendering is active */ bool isRenderingActive() const; diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index ecd14d8b..ca9fa3fd 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -83,7 +83,7 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Pre-allocate memory pools to prevent allocation during rendering + // Optionally pre-allocate initial memory blocks for pools if (!memoryPool->preAllocatePools()) { std::cerr << "Failed to pre-allocate memory pools" << std::endl; return false; diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 74620f8a..860eee38 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -505,7 +505,7 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity // Render the scene void Renderer::Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { - // Set rendering active to prevent memory pool growth during rendering + // Mark rendering as active (informational flag for systems that care) if (memoryPool) { memoryPool->setRenderingActive(true); } diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 61b0ba21..b9f2540d 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1420,12 +1420,6 @@ std::pair Renderer::createBuffer( throw std::runtime_error("Memory pool not available - cannot create buffer"); } - // Check if we're trying to allocate during rendering - if (memoryPool->isRenderingActive()) { - std::cerr << "ERROR: Attempted to create buffer during rendering! Size: " << size << " bytes" << std::endl; - std::cerr << "This violates the constraint that no new memory should be allocated during rendering." << std::endl; - throw std::runtime_error("Buffer creation attempted during rendering - this is not allowed"); - } // Only allow direct allocation for staging buffers (temporary, host-visible) if (!(properties & vk::MemoryPropertyFlagBits::eHostVisible)) { From b167c70dc1d218a13e8ba4bd76d88bdc52f0c12c Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 19 Aug 2025 04:25:55 -0700 Subject: [PATCH 094/102] addressing more comments. --- attachments/simple_engine/install_dependencies_windows.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index 2bcfc2e2..0fe383f4 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -26,7 +26,7 @@ if not exist %TEMP%\vcpkg-cache mkdir %TEMP%\vcpkg-cache :: Install all dependencies at once using vcpkg with parallel installation echo Installing all dependencies... -vcpkg install --triplet=x64-windows --x-manifest-root=%~dp0\.. --feature-flags=binarycaching,manifests --x-install-root=%VCPKG_INSTALLATION_ROOT%/installed +vcpkg install --triplet=x64-windows --x-manifest-root=%~dp0 --feature-flags=binarycaching,manifests --x-install-root=%VCPKG_INSTALLATION_ROOT%/installed :: Remind about Vulkan SDK echo. From 276d1cb5c861b21e45182448130be9a2f077fda2 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 20 Aug 2025 17:06:57 -0700 Subject: [PATCH 095/102] addressing more comments. --- attachments/simple_engine/engine.cpp | 2 +- attachments/simple_engine/engine.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 4fc3fb91..a9349e6f 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -191,7 +191,7 @@ Entity* Engine::CreateEntity(const std::string& name) { return rawPtr; } -Entity* Engine::GetEntity(const std::string& name) const { +Entity* Engine::GetEntity(const std::string& name) { auto it = entityMap.find(name); if (it != entityMap.end()) { return it->second; diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 0e142407..f8eede89 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -67,7 +67,7 @@ class Engine { * @param name The name of the entity. * @return A pointer to the entity, or nullptr if not found. */ - Entity* GetEntity(const std::string& name) const; + Entity* GetEntity(const std::string& name); /** * @brief Remove an entity. From 6e8adb4a39f402e22303f3332107c32f2916d6ec Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 25 Aug 2025 01:52:20 -0700 Subject: [PATCH 096/102] implementing Spec-Gloss workflow and transmission materials. --- attachments/simple_engine/model_loader.cpp | 114 +++++++++ attachments/simple_engine/model_loader.h | 13 +- attachments/simple_engine/pipeline.cpp | 72 +++--- attachments/simple_engine/pipeline.h | 4 + attachments/simple_engine/renderer.h | 5 + .../simple_engine/renderer_pipelines.cpp | 70 ++++-- .../simple_engine/renderer_rendering.cpp | 225 +++++++++++++++++- .../simple_engine/renderer_resources.cpp | 4 +- attachments/simple_engine/shaders/pbr.slang | 109 +++++++-- 9 files changed, 537 insertions(+), 79 deletions(-) diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 7ce6fdf4..ffef49f8 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -230,6 +230,120 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { material->emissiveStrength = 1.0f * light_scale; } + // Alpha mode / cutoff + material->alphaMode = gltfMaterial.alphaMode.empty() ? std::string("OPAQUE") : gltfMaterial.alphaMode; + material->alphaCutoff = static_cast(gltfMaterial.alphaCutoff); + + // Transmission (KHR_materials_transmission) + auto transIt = gltfMaterial.extensions.find("KHR_materials_transmission"); + if (transIt != gltfMaterial.extensions.end()) { + const tinygltf::Value& ext = transIt->second; + if (ext.Has("transmissionFactor") && ext.Get("transmissionFactor").IsNumber()) { + material->transmissionFactor = static_cast(ext.Get("transmissionFactor").Get()); + } + } + + // Specular-Glossiness (KHR_materials_pbrSpecularGlossiness) + auto sgIt = gltfMaterial.extensions.find("KHR_materials_pbrSpecularGlossiness"); + if (sgIt != gltfMaterial.extensions.end()) { + const tinygltf::Value& ext = sgIt->second; + material->useSpecularGlossiness = true; + // diffuseFactor -> albedo and alpha + if (ext.Has("diffuseFactor") && ext.Get("diffuseFactor").IsArray()) { + const auto& arr = ext.Get("diffuseFactor").Get(); + if (arr.size() >= 3) { + material->albedo = glm::vec3( + arr[0].IsNumber() ? static_cast(arr[0].Get()) : material->albedo.r, + arr[1].IsNumber() ? static_cast(arr[1].Get()) : material->albedo.g, + arr[2].IsNumber() ? static_cast(arr[2].Get()) : material->albedo.b + ); + if (arr.size() >= 4 && arr[3].IsNumber()) { + material->alpha = static_cast(arr[3].Get()); + } + } + } + // specularFactor (vec3) + if (ext.Has("specularFactor") && ext.Get("specularFactor").IsArray()) { + const auto& arr = ext.Get("specularFactor").Get(); + if (arr.size() >= 3) { + material->specularFactor = glm::vec3( + arr[0].IsNumber() ? static_cast(arr[0].Get()) : material->specularFactor.r, + arr[1].IsNumber() ? static_cast(arr[1].Get()) : material->specularFactor.g, + arr[2].IsNumber() ? static_cast(arr[2].Get()) : material->specularFactor.b + ); + } + } + // glossinessFactor (float) + if (ext.Has("glossinessFactor") && ext.Get("glossinessFactor").IsNumber()) { + material->glossinessFactor = static_cast(ext.Get("glossinessFactor").Get()); + } + + // Load diffuseTexture into albedoTexturePath if present + if (ext.Has("diffuseTexture") && ext.Get("diffuseTexture").IsObject()) { + const auto& diffObj = ext.Get("diffuseTexture"); + if (diffObj.Has("index") && diffObj.Get("index").IsInt()) { + int texIndex = diffObj.Get("index").Get(); + if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) { + const auto& texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) { + imageIndex = texture.source; + } else { + auto extBasis = texture.extensions.find("KHR_texture_basisu"); + if (extBasis != texture.extensions.end()) { + const tinygltf::Value &e = extBasis->second; + if (e.Has("source") && e.Get("source").IsInt()) { + int src = e.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) imageIndex = src; + } + } + } + if (imageIndex >= 0) { + const auto& image = gltfModel.images[imageIndex]; + std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { + material->albedoTexturePath = textureId; + } + } else if (!image.uri.empty()) { + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + material->albedoTexturePath = textureId; + } + } + } + } + } + } + // Load specularGlossinessTexture into specGlossTexturePath and mirror to metallicRoughnessTexturePath (binding 2) + if (ext.Has("specularGlossinessTexture") && ext.Get("specularGlossinessTexture").IsObject()) { + const auto& sgObj = ext.Get("specularGlossinessTexture"); + if (sgObj.Has("index") && sgObj.Get("index").IsInt()) { + int texIndex = sgObj.Get("index").Get(); + if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) { + const auto& texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) { + std::string textureId = "gltf_specGloss_" + std::to_string(texIndex); + const auto& image = gltfModel.images[texture.source]; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { + material->specGlossTexturePath = textureId; + material->metallicRoughnessTexturePath = textureId; // reuse binding 2 + } + } else if (!image.uri.empty()) { + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + material->specGlossTexturePath = textureId; + material->metallicRoughnessTexturePath = textureId; // reuse binding 2 + } + } + } + } + } + } + } // Extract texture information and load embedded texture data if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index 57bd345b..56787d9b 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -25,7 +25,7 @@ class Material { [[nodiscard]] const std::string& GetName() const { return name; } - // PBR properties + // PBR properties (Metallic-Roughness default) glm::vec3 albedo = glm::vec3(1.0f); float metallic = 0.0f; float roughness = 1.0f; @@ -33,6 +33,17 @@ class Material { glm::vec3 emissive = glm::vec3(0.0f); float emissiveStrength = 1.0f; // KHR_materials_emissive_strength extension float alpha = 1.0f; // Base color alpha (from MR baseColorFactor or SpecGloss diffuseFactor) + float transmissionFactor = 0.0f; // KHR_materials_transmission: 0=opaque, 1=fully transmissive + + // Specular-Glossiness workflow (KHR_materials_pbrSpecularGlossiness) + bool useSpecularGlossiness = false; + glm::vec3 specularFactor = glm::vec3(0.04f); + float glossinessFactor = 1.0f; + std::string specGlossTexturePath; // Stored separately; also mirrored to metallicRoughnessTexturePath for binding 2 + + // Alpha handling (glTF alphaMode and cutoff) + std::string alphaMode = "OPAQUE"; // "OPAQUE", "MASK", or "BLEND" + float alphaCutoff = 0.5f; // Used when alphaMode == MASK // Texture paths for PBR materials std::string albedoTexturePath; diff --git a/attachments/simple_engine/pipeline.cpp b/attachments/simple_engine/pipeline.cpp index 6c632b4a..ac44c3b3 100644 --- a/attachments/simple_engine/pipeline.cpp +++ b/attachments/simple_engine/pipeline.cpp @@ -1,4 +1,5 @@ #include "pipeline.h" +#include "mesh_component.h" #include #include @@ -52,7 +53,7 @@ bool Pipeline::createDescriptorSetLayout() { bool Pipeline::createPBRDescriptorSetLayout() { try { // Create descriptor set layout bindings for PBR shader - std::array bindings = { + std::array bindings = { // Binding 0: Uniform buffer (UBO) vk::DescriptorSetLayoutBinding{ .binding = 0, @@ -100,6 +101,14 @@ bool Pipeline::createPBRDescriptorSetLayout() { .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr + }, + // Binding 6: Light storage buffer (StructuredBuffer) + vk::DescriptorSetLayoutBinding{ + .binding = 6, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr } }; @@ -330,49 +339,28 @@ bool Pipeline::createPBRPipeline() { vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - // Define vertex binding description - vk::VertexInputBindingDescription bindingDescription{ - .binding = 0, - .stride = sizeof(float) * (3 + 3 + 2 + 4), // Position(3) + Normal(3) + UV(2) + Tangent(4) - .inputRate = vk::VertexInputRate::eVertex - }; - - // Define vertex attribute descriptions - std::array attributeDescriptions = { - // Position attribute - vk::VertexInputAttributeDescription{ - .location = 0, - .binding = 0, - .format = vk::Format::eR32G32B32Sfloat, - .offset = 0 - }, - // Normal attribute - vk::VertexInputAttributeDescription{ - .location = 1, - .binding = 0, - .format = vk::Format::eR32G32B32Sfloat, - .offset = sizeof(float) * 3 - }, - // UV attribute - vk::VertexInputAttributeDescription{ - .location = 2, - .binding = 0, - .format = vk::Format::eR32G32Sfloat, - .offset = sizeof(float) * 6 - }, - // Tangent attribute - vk::VertexInputAttributeDescription{ - .location = 3, - .binding = 0, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = sizeof(float) * 8 - } - }; + // Define vertex and instance binding descriptions using MeshComponent layouts + auto vertexBinding = Vertex::getBindingDescription(); + auto instanceBinding = InstanceData::getBindingDescription(); + std::array bindingDescriptions = { vertexBinding, instanceBinding }; + + // Define vertex and instance attribute descriptions + auto vertexAttrArray = Vertex::getAttributeDescriptions(); + auto instanceAttrArray = InstanceData::getAttributeDescriptions(); + std::array attributeDescriptions{}; + // Copy vertex attributes (0..3) + for (size_t i = 0; i < vertexAttrArray.size(); ++i) { + attributeDescriptions[i] = vertexAttrArray[i]; + } + // Copy instance attributes (4..10) + for (size_t i = 0; i < instanceAttrArray.size(); ++i) { + attributeDescriptions[vertexAttrArray.size() + i] = instanceAttrArray[i]; + } // Create vertex input info vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = 1, - .pVertexBindingDescriptions = &bindingDescription, + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), .pVertexAttributeDescriptions = attributeDescriptions.data() }; @@ -425,7 +413,7 @@ bool Pipeline::createPBRPipeline() { .sampleShadingEnable = VK_FALSE, .minSampleShading = 1.0f, .pSampleMask = nullptr, - .alphaToCoverageEnable = VK_FALSE, + .alphaToCoverageEnable = VK_TRUE, .alphaToOneEnable = VK_FALSE }; diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h index a2724f90..b4ad8f6d 100644 --- a/attachments/simple_engine/pipeline.h +++ b/attachments/simple_engine/pipeline.h @@ -27,6 +27,10 @@ struct MaterialProperties { alignas(4) float alphaMaskCutoff; alignas(16) glm::vec3 emissiveFactor; // Emissive factor for HDR emissive sources alignas(4) float emissiveStrength; // KHR_materials_emissive_strength extension + alignas(4) float transmissionFactor; // KHR_materials_transmission + alignas(4) int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness + alignas(4) float glossinessFactor; // SpecGloss glossiness scalar + alignas(16) glm::vec3 specularFactor; // SpecGloss specular color factor }; /** diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index b2cd879d..14077b38 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -96,6 +96,10 @@ struct MaterialProperties { alignas(4) float alphaMaskCutoff; alignas(16) glm::vec3 emissiveFactor; // Emissive factor for HDR emissive sources alignas(4) float emissiveStrength; // KHR_materials_emissive_strength extension + alignas(4) float transmissionFactor; // KHR_materials_transmission + alignas(4) int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness + alignas(4) float glossinessFactor; // SpecGloss glossiness scalar + alignas(16) glm::vec3 specularFactor; // SpecGloss specular color factor }; /** @@ -460,6 +464,7 @@ class Renderer { vk::raii::Pipeline graphicsPipeline = nullptr; vk::raii::PipelineLayout pbrPipelineLayout = nullptr; vk::raii::Pipeline pbrGraphicsPipeline = nullptr; + vk::raii::Pipeline pbrBlendGraphicsPipeline = nullptr; vk::raii::PipelineLayout lightingPipelineLayout = nullptr; vk::raii::Pipeline lightingPipeline = nullptr; diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index ef3375a0..7924d372 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -422,15 +422,6 @@ bool Renderer::createPBRPipeline() { pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); - // Enable alpha blending for translucency (glass) - colorBlendAttachment.blendEnable = VK_TRUE; - colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; - colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; - colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd; - colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; - colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; - colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd; - // Create pipeline rendering info vk::Format depthFormat = findDepthFormat(); @@ -444,8 +435,19 @@ bool Renderer::createPBRPipeline() { .stencilAttachmentFormat = vk::Format::eUndefined }; - // Create a graphics pipeline - vk::GraphicsPipelineCreateInfo pipelineInfo{ + // 1) Opaque PBR pipeline (no blending, depth writes enabled) + vk::PipelineColorBlendAttachmentState opaqueBlendAttachment = colorBlendAttachment; + opaqueBlendAttachment.blendEnable = VK_FALSE; + vk::PipelineColorBlendStateCreateInfo colorBlendingOpaque{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &opaqueBlendAttachment + }; + vk::PipelineDepthStencilStateCreateInfo depthStencilOpaque = depthStencil; + depthStencilOpaque.depthWriteEnable = VK_TRUE; + + vk::GraphicsPipelineCreateInfo opaquePipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &pbrPipelineRenderingCreateInfo, .flags = vk::PipelineCreateFlags{}, @@ -456,8 +458,48 @@ bool Renderer::createPBRPipeline() { .pViewportState = &viewportState, .pRasterizationState = &rasterizer, .pMultisampleState = &multisampling, - .pDepthStencilState = &depthStencil, - .pColorBlendState = &colorBlending, + .pDepthStencilState = &depthStencilOpaque, + .pColorBlendState = &colorBlendingOpaque, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + pbrGraphicsPipeline = vk::raii::Pipeline(device, nullptr, opaquePipelineInfo); + + // 2) Blended PBR pipeline (alpha blending, depth writes disabled for translucency) + vk::PipelineColorBlendAttachmentState blendedAttachment = colorBlendAttachment; + blendedAttachment.blendEnable = VK_TRUE; + blendedAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; + blendedAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + blendedAttachment.colorBlendOp = vk::BlendOp::eAdd; + blendedAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; + blendedAttachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + blendedAttachment.alphaBlendOp = vk::BlendOp::eAdd; + vk::PipelineColorBlendStateCreateInfo colorBlendingBlended{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &blendedAttachment + }; + vk::PipelineDepthStencilStateCreateInfo depthStencilBlended = depthStencil; + depthStencilBlended.depthWriteEnable = VK_FALSE; + + vk::GraphicsPipelineCreateInfo blendedPipelineInfo{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilBlended, + .pColorBlendState = &colorBlendingBlended, .pDynamicState = &dynamicState, .layout = *pbrPipelineLayout, .renderPass = nullptr, @@ -465,8 +507,8 @@ bool Renderer::createPBRPipeline() { .basePipelineHandle = nullptr, .basePipelineIndex = -1 }; + pbrBlendGraphicsPipeline = vk::raii::Pipeline(device, nullptr, blendedPipelineInfo); - pbrGraphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); return true; } catch (const std::exception& e) { std::cerr << "Failed to create PBR pipeline: " << e.what() << std::endl; diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 860eee38..11ae9003 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -497,7 +497,7 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity ubo.exposure = this->exposure; ubo.gamma = this->gamma; ubo.prefilteredCubeMipLevels = 0.0f; - ubo.scaleIBLAmbient = 1.0f; + ubo.scaleIBLAmbient = 0.5f; // Copy to uniform buffer std::memcpy(entityIt->second.uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); @@ -606,6 +606,7 @@ void Renderer::Render(const std::vector>& entities, Came // Track current pipeline to avoid unnecessary bindings vk::raii::Pipeline* currentPipeline = nullptr; vk::raii::PipelineLayout* currentLayout = nullptr; + std::vector blendedQueue; // Render each entity for (auto const& uptr : entities) { @@ -642,8 +643,47 @@ void Renderer::Render(const std::vector>& entities, Came // Use basic pipeline only when PBR is explicitly disabled via ImGui bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); bool usePBR = !useBasic; // BRDF/PBR is now the default lighting model - vk::raii::Pipeline* selectedPipeline = usePBR ? &pbrGraphicsPipeline : &graphicsPipeline; - vk::raii::PipelineLayout* selectedLayout = usePBR ? &pbrPipelineLayout : &pipelineLayout; + + // Choose PBR pipeline variant per material (BLEND -> blended pipeline) + vk::raii::Pipeline* selectedPipeline = nullptr; + vk::raii::PipelineLayout* selectedLayout = nullptr; + if (usePBR) { + bool useBlended = false; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + if (material->alphaMode == "BLEND") { + useBlended = true; + } else if (material->alphaMode != "MASK" && material->transmissionFactor > 0.001f) { + // Use blended pipeline for transmissive materials + useBlended = true; + } else if (material->useSpecularGlossiness && material->alpha < 0.999f) { + // SpecGloss glass with alpha < 1 should blend + useBlended = true; + } + } + } + } + } + // Defer blended/transmissive materials to a second pass + if (useBlended) { + blendedQueue.push_back(entity); + continue; + } + // Opaques use the non-blended PBR pipeline in this first pass + selectedPipeline = &pbrGraphicsPipeline; + selectedLayout = &pbrPipelineLayout; + } else { + selectedPipeline = &graphicsPipeline; + selectedLayout = &pipelineLayout; + } // Get the mesh resources - they should already exist from pre-allocation auto meshIt = meshResources.find(meshComponent); @@ -723,6 +763,20 @@ void Renderer::Render(const std::vector>& entities, Came pushConstants.roughnessFactor = material->roughness; pushConstants.emissiveFactor = material->emissive; // Set emissive factor for HDR emissive sources pushConstants.emissiveStrength = material->emissiveStrength; // Set emissive strength from KHR_materials_emissive_strength extension + pushConstants.transmissionFactor = material->transmissionFactor; // KHR_materials_transmission + // SpecGloss workflow push constants + if (material->useSpecularGlossiness) { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.specularFactor = material->specularFactor; + pushConstants.glossinessFactor = material->glossinessFactor; + // If no SpecGloss texture, signal shader to use factors-only path + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + } else { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + pushConstants.physicalDescriptorTextureSet = 0; + } } else { // Default PBR material properties pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); @@ -740,6 +794,11 @@ void Renderer::Render(const std::vector>& entities, Came pushConstants.roughnessFactor = 0.7f; pushConstants.emissiveFactor = glm::vec3(0.0f); pushConstants.emissiveStrength = 0.0f; + pushConstants.transmissionFactor = 0.0f; + // Default to MR workflow + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; } // Set texture binding indices @@ -753,8 +812,29 @@ void Renderer::Render(const std::vector>& entities, Came emissiveSet = 0; } pushConstants.emissiveTextureSet = emissiveSet; + // Alpha mask from glTF material pushConstants.alphaMask = 0.0f; pushConstants.alphaMaskCutoff = 0.5f; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + if (material->alphaMode == "MASK") { + pushConstants.alphaMask = 1.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } else { + pushConstants.alphaMask = 0.0f; // OPAQUE or BLEND not handled here + } + } + } + } + } // Push constants to the shader commandBuffers[currentFrame].pushConstants( @@ -769,6 +849,145 @@ void Renderer::Render(const std::vector>& entities, Came commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); } + // Second pass: render blended/transmissive materials after opaque + if (!blendedQueue.empty()) { + for (Entity* entity : blendedQueue) { + if (!entity || !entity->IsActive()) { continue; } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { continue; } + auto transformComponent = entity->GetComponent(); + if (!transformComponent) { continue; } + + // Mesh & entity resources + auto meshIt = meshResources.find(meshComponent); + if (meshIt == meshResources.end()) { + std::cerr << "ERROR: Mesh resources not found for blended entity " << entity->GetName() << std::endl; + continue; + } + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + std::cerr << "ERROR: Entity resources not found for blended entity " << entity->GetName() << std::endl; + continue; + } + + // Use blended PBR pipeline + vk::raii::Pipeline* selectedPipeline = &pbrBlendGraphicsPipeline; + vk::raii::PipelineLayout* selectedLayout = &pbrPipelineLayout; + if (currentPipeline != selectedPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + } + + // Bind vertex + instance buffers + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + + // Update UBO for this entity + updateUniformBuffer(currentFrame, entity, camera); + + // Bind index buffer + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + + // Bind descriptor set (PBR) + auto& selectedDescriptorSets = entityIt->second.pbrDescriptorSets; + if (selectedDescriptorSets.empty() || currentFrame >= selectedDescriptorSets.size()) { + std::cerr << "Error: No valid PBR descriptor set for blended entity " << entity->GetName() << std::endl; + continue; + } + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {} + ); + + // Push PBR material properties (same as first pass) + MaterialProperties pushConstants{}; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + pushConstants.emissiveFactor = material->emissive; + pushConstants.emissiveStrength = material->emissiveStrength; + pushConstants.transmissionFactor = material->transmissionFactor; + if (material->useSpecularGlossiness) { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.specularFactor = material->specularFactor; + pushConstants.glossinessFactor = material->glossinessFactor; + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + } else { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + pushConstants.physicalDescriptorTextureSet = 0; + } + } else { + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; + } + } + } + } else { + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 0.0f; + pushConstants.transmissionFactor = 0.0f; + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + } + pushConstants.baseColorTextureSet = 0; + pushConstants.physicalDescriptorTextureSet = 0; + pushConstants.normalTextureSet = 0; + pushConstants.occlusionTextureSet = 0; + int emissiveSet = -1; + if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) { emissiveSet = 0; } + pushConstants.emissiveTextureSet = emissiveSet; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material && material->alphaMode == "MASK") { + pushConstants.alphaMask = 1.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } + } + } + } + + commandBuffers[currentFrame].pushConstants( + **currentLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + vk::ArrayProxy(sizeof(MaterialProperties), reinterpret_cast(&pushConstants)) + ); + + uint32_t instanceCount = static_cast(std::max(1u, static_cast(meshComponent->GetInstanceCount()))); + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); + } + } + // Render ImGui if provided if (imguiSystem) { imguiSystem->Render(commandBuffers[currentFrame], currentFrame); diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index b9f2540d..320a19b2 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -562,11 +562,13 @@ vk::Format Renderer::determineTextureFormat(const std::string& textureId) { std::string idLower = textureId; std::transform(idLower.begin(), idLower.end(), idLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - // BaseColor/Albedo/Diffuse textures should be in sRGB space for proper gamma correction + // BaseColor/Albedo/Diffuse & SpecGloss RGB should be sRGB for proper gamma correction if (idLower.find("basecolor") != std::string::npos || idLower.find("base_color") != std::string::npos || idLower.find("albedo") != std::string::npos || idLower.find("diffuse") != std::string::npos || + idLower.find("specgloss") != std::string::npos || + idLower.find("specularglossiness") != std::string::npos || textureId == Renderer::SHARED_DEFAULT_ALBEDO_ID) { return vk::Format::eR8G8B8A8Srgb; } diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index e64e671a..5d3fd564 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -64,6 +64,10 @@ struct PushConstants { float alphaMaskCutoff; float3 emissiveFactor; // Emissive factor for HDR emissive sources float emissiveStrength; // KHR_materials_emissive_strength extension + float transmissionFactor; // KHR_materials_transmission + int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness + float glossinessFactor; // SpecGloss glossiness scalar + float3 specularFactor; // SpecGloss specular color factor }; // Constants @@ -115,7 +119,7 @@ VSOutput VSMain(VSInput input) // Use instance matrices directly float4x4 instanceModelMatrix = input.InstanceModelMatrix; - float3x3 normalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; + float3x3 instanceNormalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; // Transform position to world space: entity model * instance model float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); @@ -124,15 +128,18 @@ VSOutput VSMain(VSInput input) // Pass world position to fragment shader output.WorldPos = worldPos.xyz; - // Transform normal to world space using reconstructed normal matrix and entity model + // Transform normal and tangent to world space (apply instance normal then entity model) float3x3 model3x3 = (float3x3)ubo.model; - output.Normal = normalize(mul(model3x3, mul(normalMatrix3x3, input.Normal))); + output.Normal = normalize(mul(model3x3, mul(instanceNormalMatrix3x3, input.Normal))); + + float3 instTangent = mul(instanceNormalMatrix3x3, input.Tangent.xyz); + float3 worldTangent = normalize(mul(model3x3, instTangent)); // Pass texture coordinates output.UV = input.UV; - // Pass tangent - output.Tangent = input.Tangent; + // Pass world-space tangent (preserve handedness in w) + output.Tangent = float4(worldTangent, input.Tangent.w); return output; } @@ -144,22 +151,66 @@ float4 PSMain(VSOutput input) : SV_TARGET // Sample material textures (flip V to match glTF UV origin) float2 uv = float2(input.UV.x, 1.0 - input.UV.y); float4 baseColor = baseColorMap.Sample(uv) * material.baseColorFactor; - float2 metallicRoughness = metallicRoughnessMap.Sample(uv).bg; - float metallic = metallicRoughness.x * material.metallicFactor; - float roughness = metallicRoughness.y * material.roughnessFactor; + // For MR workflow: metallic in B, roughness in G; For SpecGloss: RGB=specular color, A=glossiness + float4 mrOrSpecGloss = (material.physicalDescriptorTextureSet < 0) ? float4(1.0, 1.0, 1.0, 1.0) : metallicRoughnessMap.Sample(uv); + float metallic = 0.0; + float roughness = 1.0; + float3 specColorSG = float3(0.0, 0.0, 0.0); + if (material.useSpecGlossWorkflow != 0) { + // Specular-Glossiness workflow + specColorSG = mrOrSpecGloss.rgb * material.specularFactor; + float gloss = clamp(mrOrSpecGloss.a * material.glossinessFactor, 0.0, 1.0); + roughness = clamp(1.0 - gloss, 0.0, 1.0); + metallic = 0.0; // not used in SpecGloss + } else { + // Metallic-Roughness workflow + float2 metallicRoughness = mrOrSpecGloss.bg; + metallic = clamp(metallicRoughness.x * material.metallicFactor, 0.0, 1.0); + roughness = clamp(metallicRoughness.y * material.roughnessFactor, 0.0, 1.0); + } float ao = occlusionMap.Sample(uv).r; float3 emissiveTex = (material.emissiveTextureSet < 0) ? float3(1.0, 1.0, 1.0) : emissiveMap.Sample(uv).rgb; float3 emissive = emissiveTex * material.emissiveFactor * material.emissiveStrength; + // Early alpha masking discard for masked materials only (glTF alphaMode == MASK) + if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { + discard; + } + // Calculate normal in tangent space float3 N = normalize(input.Normal); if (material.normalTextureSet >= 0) { // Apply normal mapping float3 tangentNormal = normalMap.Sample(uv).xyz * 2.0 - 1.0; - float3 T = normalize(input.Tangent.xyz); - float3 B = normalize(cross(N, T)) * input.Tangent.w; + + float3 T = input.Tangent.xyz; + bool hasTangent = dot(T, T) > 1e-6; + if (hasTangent) { + // Orthonormalize T against N to reduce shading artifacts + T = normalize(T); + T = normalize(T - N * dot(N, T)); + } else { + // Fallback: derive tangent from screen-space derivatives of position and UVs + float3 dp1 = ddx(input.WorldPos); + float3 dp2 = ddy(input.WorldPos); + float2 duv1 = ddx(uv); + float2 duv2 = ddy(uv); + float det = duv1.x * duv2.y - duv1.y * duv2.x; + if (abs(det) > 1e-8) { + float r = 1.0 / det; + T = normalize((dp1 * duv2.y - dp2 * duv1.y) * r); + } else { + // Degenerate UV derivatives; fall back to a stable orthogonal vector + float3 up = (abs(N.y) < 0.999) ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); + T = normalize(cross(up, N)); + } + } + float handedness = hasTangent ? input.Tangent.w : 1.0; + float3 B = normalize(cross(N, T)) * handedness; + // Construct tangent-to-world with T, B, N basis float3x3 TBN = float3x3(T, B, N); - N = normalize(mul(tangentNormal, TBN)); + // Transform from tangent to world space using column-vector convention + N = normalize(mul(TBN, tangentNormal)); } // Calculate view and reflection vectors @@ -167,8 +218,15 @@ float4 PSMain(VSOutput input) : SV_TARGET float3 R = reflect(-V, N); // Calculate F0 (base reflectivity) - float3 F0 = float3(0.04, 0.04, 0.04); - F0 = lerp(F0, baseColor.rgb, metallic); + float3 F0; + if (material.useSpecGlossWorkflow != 0) { + // SpecGloss: use specular color directly + F0 = specColorSG; + } else { + // MR: interpolate between dielectric and baseColor by metallic + F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + } // Initialize lighting float3 Lo = float3(0.0, 0.0, 0.0); @@ -263,19 +321,34 @@ float4 PSMain(VSOutput input) : SV_TARGET } } - // Add only emissive (no hardcoded ambient - use only model-defined lights) - // Use full emissive contribution for proper HDR rendering - float3 color = Lo + emissive; + float3 ambient = baseColor.rgb * ao * (0.03 * ubo.scaleIBLAmbient); + // Base lit color from direct lighting and emissive + float3 opaqueLit = ambient + Lo + emissive; + + // Transmission respecting roughness and Fresnel, but without environment map + float T = clamp(material.transmissionFactor, 0.0, 1.0); + float NdotV_glass = max(dot(N, V), 0.0); + float3 Fv = FresnelSchlick(NdotV_glass, F0); + float Favg = (Fv.x + Fv.y + Fv.z) / 3.0; + float roughTrans = clamp(1.0 - (roughness * roughness), 0.0, 1.0); + float T_eff = T * (1.0 - Favg) * roughTrans; + + // Energy-conserving mix between opaque lighting and transmitted base color tint + float3 color = lerp(opaqueLit, baseColor.rgb, T_eff); // Apply exposure before tone mapping for proper HDR workflow color *= ubo.exposure; // Standard Reinhard tone mapping - simple and effective - // This prevents excessive brightness while preserving color relationships color = color / (1.0 + color); // Gamma correction (convert from linear to sRGB) color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); - return float4(color, baseColor.a); + // Alpha approximates remaining opacity (higher transmission -> lower alpha), clamped for readability + float alphaOut = baseColor.a; + if (T > 0.001) { + alphaOut = clamp(1.0 - T_eff, 0.08, 0.60); + } + return float4(color, alphaOut); } From e1ac508ec5a19ebffbc062c2e4f536c23a5d0359 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 25 Aug 2025 02:14:27 -0700 Subject: [PATCH 097/102] updating nlohmann_json to v3.12.0. --- attachments/CMake/Findnlohmann_json.cmake | 2 +- attachments/CMake/Findtinygltf.cmake | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/attachments/CMake/Findnlohmann_json.cmake b/attachments/CMake/Findnlohmann_json.cmake index 287c7e83..61dc66a6 100644 --- a/attachments/CMake/Findnlohmann_json.cmake +++ b/attachments/CMake/Findnlohmann_json.cmake @@ -47,7 +47,7 @@ if(NOT nlohmann_json_INCLUDE_DIR) FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git - GIT_TAG v3.11.2 # Use a specific tag for stability + GIT_TAG v3.12.0 # Use a specific tag for stability ) # Set policy to suppress the deprecation warning diff --git a/attachments/CMake/Findtinygltf.cmake b/attachments/CMake/Findtinygltf.cmake index 35d132c2..b2412350 100644 --- a/attachments/CMake/Findtinygltf.cmake +++ b/attachments/CMake/Findtinygltf.cmake @@ -16,11 +16,11 @@ find_package(nlohmann_json QUIET) if(NOT nlohmann_json_FOUND) include(FetchContent) - message(STATUS "nlohmann_json not found, fetching from GitHub...") + message(STATUS "nlohmann_json not found, fetching v3.12.0 from GitHub...") FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git - GIT_TAG v3.11.2 # Use a specific tag for stability + GIT_TAG v3.12.0 # Use a specific tag for stability ) FetchContent_MakeAvailable(nlohmann_json) endif() From bbbae416d882b277773e93bd3920eed8e70e708c Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 25 Aug 2025 14:58:15 -0700 Subject: [PATCH 098/102] update the default screenshot. --- images/bistro.png | Bin 735878 -> 1093206 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/bistro.png b/images/bistro.png index 7dbe9cf841ccbbb590c821f881f550a291e1cb89..ca248e990897fbaf5d90c535f2c2c065aec9b3a0 100644 GIT binary patch literal 1093206 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øsGOg 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%0DhtTJSBxch*ZC*q;k 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

Z)tF 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

-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 z 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

!;HKyc@ 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%0A$MLN8wu6#o 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

9qga^d;G`O7C$4rReKO}KsWoY~pMNoLAg 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

GnwRk>=RU%I48#;gL48=GRg6v}vdGZX4QE%6 z{oF~gvg=x`l}3Qh6XZluOrB414j+hAv~A^Ow-mVtr$E+C(KIEK$=o-rln8cH736Wm z&GtYVLs3pD_(o;eZ+A@Sqd~r$nzvaEpyZ$w7nurwwoJb zZOG^6!@dpYVyrKM?AdNtfCU$M3ft4bZrgU0yE}pI)iv>C;m@C=Z7TwFUFv?1at+T4 z80-rnmyvf_tzPi<+t11Ke8@1~4-N%Z(eOTFXXO6%v5$T&uRnO5U;X^&SUz|_l1xx4 z1P2Xm+Yiw1?cFu=^9Np*2t|50zvFct_Q8HjTNQLplV)OL8xyg+dqFQ`fCGaIE)?8$#Dk5nhJ<`WdsulI>>2)uUHA z9BL+$1ZfBs)3(Pb=j!%3X)FzSX3Gn*I7P=XcXxOEe|)`p%w@}Z*7vJgwWd9PV|SlE z)AjW|_j5AOs|k1Trw17ziK}WAKnfAS)zH z9tj}{$l!2ou6@tx)7{^&_qWG2S5^L~RcqI`&k0*xo? z8;ZK&$>kI3J^*pZjBB6;*R=YDFZ>i)q{yb{}S4&izAD#ErM z?T~QP}MxzFC7@MQ-n?k!Iyr?r+N3C zclge~@;`8X^^`b?u^N&%Mq}tYF&L;J`2jj>SuJnS#xR>L@B^5XO8C~_@NY9oW^6Yb z;$D$VcZEBk)?kfGB0HmrbVp-Tphm?pRaKIuQ`|IRi}HxMJtBqApFgLrYdYQWqd)Rv ze8pFMmOuX&|1woo@wGqyYgj)#P*xSs-*|(ns)oUy|MbWI8LQR9Bi^&ySC1(cVdcXI zcp+NqgPGF+CkzzB#Ks2?VPJBqrSHG*X<^}dhDkz*6#K+D1W3aD6dK16&GGB#->+?3 zcDo(3Ub@`hU-%rn0ar?qWibz%y=#;fzIFP@t(V4ip<4??9eJF8Yyf+@uaEZ#QaU8F zs@z%$nYe~$d6W|!XYtVd?q|-_^`6baR|Xms8AcjZ6b+nmaB{)gJeGMBLU4UPr!2RC zAj`&V4praF00_~~-%x$<9tKnCu>NIjuo#q-Sm7#?)(3|+!qGP(oGyJumQ66GL+f_f zOHJd{d_GP?KV0h&(FSPSvhRg^EXlgv)gY}?HWMJ6`s~m&ZiOA@wuF1mPY!Py{ph?L z_oODv@?o#LYjBmW-M(@CEW&leg!?$G1cV^U={`BF;)Y3pe-5DnTv$DTqx|`4A5~Zt z_O?1ai(w)WCR=WeTjDq!5YOSSpJ?v;6!W9v!_q^%zN*-V3-;yGm<1_Jj!Xijt@R~!QpT~ zYt5_o|0Tv+Zf@_H&!#kF{LbI`>uDZp{^*|?_UkwR03ZNKL_t*e6G$0xwm8G!lPEvH z&Z&k)k+0wxk|d@nYvO)t_i}mD2igr5L6&)1uq012HHu+ zoApqc^=lETErvE=v%JR&7)>U4rO|4?MI|}f8ph)>5`$DOIWLNWa=Rp(UQresA|$i( zYy8|Zx7Qh{RK^(UT7RVS6H@w?%Nt?=BF-_ryl&N17$b=ySM9jEx^#ebeQ&raibiYK6dy};JK%P4gaJPGK z=cexmoXj2AYOSTJc8t=DMhmhmqirqLy8TrY1yPiuwHxeO>{i6t1f^1{%Jrm;#~DRY zuwLJBb@fJ&(6x+4qhX(0W6<@UpMG&OtleoM;?JU&nGU~p3djLWrx~Aq?@M@nbB9u{ zGF}u9NFlg>^86$AMF_#>{xxwvCGN)~INRx@ksYAXU<~ZH8zLck8GqN0BQP{w&71Fh zlD4f7!r6jqzjlHP7LrtwWjTNQ@BYv+8Tm!O=oj`6bjxTo!5FaC5cLDgKJkfn*sRtp z7E|t)OBY0G#~t(~wzAk!wgxE$rYVV{6l{wX5pf)Ge)Wu^sCzW3WICPphRYeMvaHa$ z#x@nB`4orj3%brVfrCd~4O!+g$eWuFna?j6kEci(41s!7-Dqgrl5)2tNm8_S$jH^R zw|itJV!ORXU^%~dIv781K-;$IhvZ!1bm=Ws)gpu$Bz)5}rK@Yo`T!bKmN6br9tBcI zCuFF~14UUdpP%dXUR@cMo?%cB-`=bj#hLcAUeuZD@)OaXw}?b3PR(yKqDX$nz9Q?;atMXBSQineJ3efcA`C#3s? zNS1djJ_tP`na65d=*Yt774m*Vx`am&{5+S_K>O{t-sF>C^a=j%-~T~Ycee=TjLwrM zPXN{L6F%ZpU9}~~OS0bI-y?LxWN}WaB$^`y!t-~R%Nw#x@q54R+i+V{QXfjRgwbfm zYW0e`u3Y8GI=+1rB}kbNM}jmPQ`N5FOZUb*juXz#CTzBA+8VB(JmnAn=YNDWNtn$h zeA$}s_^y)eP?KN|*pA?od`G+{y( z&NOj}yZb#&H#E-s0d5vE44|9l$-tmGMPS3d<^T3*m=Laq;25mBL{!BtiY^M@P<=#% z(^S#6C5O6m*Jo`Xa683Aq8?rK=a8DoWWj#FbCp*e;kCeo5W~tT!uXlMm_ z?y65kz?;S#lMZmPtZSOLePzku<*UOzb^3BMJ$l6*A=qvYAF*$a2Gc(JzrSw3?~^1s zHvDp|Ci)6RxTilZArEO5$LDVG{T_8cP4l#$A)I*uN9v*v2K{yTb@!j;_g=Wi{Lh8J zPWU_Fdxlku)3vnLG8(yFitTngoX7CJ{O5VK!0_I~#Lg#JVb6>I%gp+NUyXBv9Xp~Z9YzrNy}I3Q(Ot#)#goBr`d`w1u)i?iZs5B4+1>hc7=wAV{+jW$CzTU9~6A zGE|b2Jai4Yn>5Z z-@hi35|NA;k5i&Zv0APeX9?Q6L0yaaB|<)yUi0XzM>D{2x}6v8*gY(fLXzbp4*Na% z?5s~dI?_~;BqN;uw`8|-f-_)=hSDtGmm;_=y-~4+w%JL z2SkxV^+E$SN|{Wj)OAA~rxZomSED|fc%1TgPXkha@8O>?3Gvi`FmVi}7`@X9f8QVS zuu0paU%%uVei3cc@`FF}qr*XLnm&=tbK-17lDMY2QJ!P0=?&eDLZOZB_C4pZ>xlgB4a4LgVEVz_9Y=&qRn)``-;`y7-_zoH~EXg+E1E!oQ$3GE4#Pz5 zv=XJ1zjilJEJUzQ6RL1Md;;g=(Cwk1s#~%w=4>|SuwUWO?yP?LLiJpDQAbQ5QM4ezbbh5!}!{^B96yH4_TNL`saK^_#5)v4NmnHc6ZByuB$jd zzo2bvx~}O5db%X6EE{U=hL%}reQSYx#zx>N2B*pA>0Ssc2caDaDGmPT{5tqx%CDdQ z96w0VKPQhqhieoj1uls6JoxA%`zxfTd|aX*2K(W9gy$;!UWkf^)T~nx75_R`L-a?g z(f<99XF%g;P5Ac;fhT`I{(XE>;Lo!ExjfH@CUO6_zvs}F_;qrh)$|~a9M&KNLi~Mw z=DkmGd2zwt`TKv5U-c`0C2<^cbMu1vY(d?WBvup2F($SMX_?Q@utu{uJ0q8pKl$hX zY5)-U%BrIwg?r}l`Io$U^%~n&2qXwcRg6`Pu?;dAQ&l^zuHG7gP5(T6a@RBstJMvQ z$+V~YreygTD{bH7;RiN5KkXDtEk#j~B+@BDb?*Jj?TX24Mx2h>ZkEjE7tnu3=uhJ~ zb}>ylW;UDk>#FgQ&q2+;#hR%3PV*LBP5n^%m}G3VD8*v|GeYJZmLlM9BK zXnB(G;>8POEXcE*G#g`Whm?Z5yL*&1WYe>L@N-O6*F-&qQ>n+*!_DfBbd=H7Es;`W z`J`WeHT;aH!5QN!OUHBX)Dgi@mJMz3z<6;voJ|M9I{;J&cgB`CH=JFZlV&;Ebo}H` z{>w-9H?+s6K%oCxXiq{Q!dlChed(7_wjD8s)q2In*#+zMk~B%^nwBVuNaGwWdb#9C z5XA|H!;U~{}L&lVi^C20~<)(1ECUUyh)u%@HKvN*dQ%1Yh)zSqv~5K`T?~y=4iO9{SPur@(_9QmqEN~i>)YGcNZo>PCB?EV5b>x#v;R1rXuK69m@z+szHR&WJOXnD4DEpogYifk_ zLD&Pni7-tQ&KA$uZMXcbANT>@d+$?26MPcKEEZ=JrIQx!x{fr<$Wm7d)<)B{Zg1_j z_f(a0UW>B@f9nVSS5$NibWekN|0gue-bwlO3=@QKO+5Gf6ooxD#HmLJY*?Z2pLtq2 z8ItYj*dQMUm)^Oas`>QakB2upZ^o0=@FfC*y1=GE)2*+rxy0xgwNdt;mtrJ_17{4HoBqJ zj|Z)W0b+RFA4lywt(uYv-k zB{2RoLthd;Z@(wQKs`KrVS?g&E{+M@W7Ru&On>poyr)-%3Hj+W3-@R^j5WN6Vb9WG zf?i7hbN_!Yzvr_o2m`JV6lp9UYPwRVHkcwDs#t4>axE=4KD)9WiBIV{(#Bn?%S)O*j z>j(OsUq62&pWn9K02;bI;AY=E+`VLRc7ad|r6RhHK2Ub^f83eu=(ZawDOgNr?2DSZ z+#wZ=vkA4<#BoZNjp#bpYZ66nke_K?6(ow>{@-kt%w{ttlWD&e@z9G9R>hC8hGo0D z2W#oHMnwr_SCQp8=V#X>i5~;tdL3r7DP5Z)RQ*f5)(vEJOHJI#D?PMT({R=2cmM;zzq zw&eWs>2Q4^5)?%dvL{m9-Mu6oMVQuN8aIVgMv~2b&E@5@o-XFlI4S9QvaZ=EY{E?< z7u??8-4n$Tby=elNj{$UsuVpRv~g4`XY9S|Kt^2`B#D!0J?vJ*aYS2nWRo%ZXza3L zBI>q9cP;zP5~UJkl9Od)loD(_t1*7vW8vk|lTS#RKb zdDKStYhgG)zv`We%ghf4r}e$v9`*XeWZ|Q98dvW0=!riB{y>IExexBbq$YHd-Wi0O z_7uGfgAHpfpZ&5g6jzJ@5n)zuZ&TK4-rO>-bg=ESkWm5u$iIKHnj zEp=V>9MOa0iO~_oIluSY{!`*4p)A%w%KorNhz4srX89TMXv$; z&9;KeiwmY{N@WW2{ERG3F~NG2)dOifr|Vi&9J|U%<#hk6s=@-x<&rc>S*}*BRu3Ew z2iBV{@4WpcKl6nT9<4JU~aPE}1pL^3qKVUpzc zi4WAgO$a|zDaC*I+kY$n)gSwBhR;4NZHJCGeBUte44a9=06YxjPJ`0WiG&oHuwvvt zE9BCL-0x0nH#js%`a@*d+vTq7hNj0bvG4&d@aQwc_Y1Ev44OkUKMYEvD8guikjDl} zf9}0K4xgich9PPnuBrd5FsVBx5w3Cfv>Ffw_+juLCfB}iz(0?{SDYk)VV^=sCGz*} zc;QF;909VRQ$Ds>*1ff5Tc){Pz=`@0trpi@X1fS%d4APe#d|CTe-Qt;XD7w z?|u}pec6|Mn)CB>*Kc9_q>q-mKK27_cRP&HxU(m zS66H{YbN6fq7__RTtaO5_CNoZ$+Ho={hD&O1z-_nWDoonarrF zmU6o!jvf1fs7Lmbl+oC!wfX9;wNCmq>;rY6s9`k9c>UsiqGZH;aRGfb-TAvvF-&9o z{y`}O_sb=;C9{i5Vij}y>NS_=V{RUH*skU5@>w6h94kvoDOR^15~&zr!77DPW8zrh zhBUi1j1_1DU3a7(Hg$nEu6i{jhKCJRUH46L7AY0RxOo#Ql8mRb9^CVQh^tz9o6>cb zEQ@=f%~j1G9&Q1rl$9i2HP6jZs_SOh`;$CpU+mpv`b1Gy6%VT=62oLN=JM*A_uu~j z2{hW0#TnUnOrqR$b16ZoWQZ$#@0He?s@kDc%y>K>=#Fjc0Bmr6-N%MiRg)wsZMk)R zElw$`l1>YvRI*rHod9nY%bV9=!63*db0REV+ac)C(#_Bc0V{uge4O;T&wcJANq=bf zKF)TYRz||#@vryZdvCBS)6t0KdhMutMl;H@M}DBHTAK2KwlmBY7mvUZ|8jEB5lYG4 z|CjynT&EuDY?_OQJc#J#)I_Y;57cGB5C3RD)B5GV74QQ7^OPZ$U{{A&jp1gsd;=SFjp>4Q)_9j|u4*NZa?TW?aQ`)wr-0zt# z7Sx9gLd4{w5#zDbT-WGc@x^7EN~M(Lp5^imYhjd6IKMbYN=4T>*|eM2Hy~Q(^XK>} zp8NgYp$d&g194<}XQUX-=A`N9BW3FSeoxmOo!w}Z(RIyWOjoNL6fsen4vxk-og5*Y zd|Op@wB?F8&M>AUn=ITR$5K}{GLFg8ltVA;T2*_8)Bt7G_P-%%r`3c=V-z{L&E0NAEMlf-bFw%^8^gL!(#GR4uJY0` znYglLQ50;~_r$5!AorZ@hPo~T1XB093!HC@+owm73GOH>r~^D7#x z5yVLXR?{@D`lHQp&+E<&VQiYl4bGI3rfuEe&$_`F&BMdmofV^b`SKNMn)34HE6&aq zynJ;t1an$zA{8NcOo0o7H^2!s*vHeC!=_vxK!=EI7`%m4nGgjEX=cV6(j?_K|E6!{ zPyOj{AA&8vhkalk_R^dpw*GfQG~U0$D#__94Bq{4Z0lOF%xZQDLdl1{H9 z48R{JK#ouDQO)qV`n?}srw?etBCUE{)`Rh6b zD7@1T?X~;AYrKx4s86^*zPjh_PWbN-Wbgrtf8VeY?DwZ%cYh5&0rSrG;{t#G3fI*q z;QnX)bN2uCYxSv5ei5Jl8ArAJ>aY4re(WFqqesuwqx_E%?8lX5pSb!p3)jgjCWd>_ zJ+C9+WUW0`Hmv2Hx8CID_Kq+5#JkMrGj_WjN-3@`uP~;h(-B|%$uA~OWB%;7e+NJ3 zXMF{4z4<1y`D|$PNwbvweovYtv_*kc5mG9ewn6s&31wALS8Ifos3amzQj~IomFF`z z1L!aOrSE!hY&ILt&dzWubUh=Z zSGlV8YbNsrX*Nb^O$Tm3Cz5nL35MQE;#gs=?!`k4QRHH?&1OlS#ULED+=Ccdmg7y9 zOU_&swc2g5wxg;$E}vdA9!=@AX1_1{{p*I-0jzF5bbi7JuAe*|&YBStVO{TxlyP4H z7LV-Be!r(IOQw0up{{#%&=_#TD~sKh{k|ZMVmi?>9gRUGY`1G9hDauKy6Qm%H!tVn z$|;N{apVRJ+Fm{vL!*y^H{LFEbxqf{?5jObo;)Ay%YgoCr$U;j1S=&<#q2k0ryv+9 z+Nz{68cEFgr4tsALOfD?^!DDJEyHHH#8}P6^^^XLf>%D(+VKASpGOEc-1;c!^zr>} zp>6VK^y89|uu}cb+i#JN#yGJB*Z&&D38w2DsDuMk-F|X$kq~fmbBCm3e)bes9SiN6 z%Nv~Hq(*B<(}2A2XZHk0d8(S#5I&l6j47VJwr-KRe4n(Ib`bI zcyCuLEG=<%hLnbC|3ESt5qE;J-ZH;_%jtT{n7S^Am`)~?`#n)Q z9wyTln+oIZo{z<7`A(9T-o~a59CydG6Ri zs`}D)T{r!`SYE#TkSLO*QARS*3te{Zs73A>N-0?`mvmjtY&Ip!CS+O6a_J`JW=Vp! zPAO$P&W6Ni_pm|>ji0pb5TuAaACqOH;a+`EwOrni#WCaA8C_M8jx&srBuRv|lAUQstSNgUI54QY~5H%;$wBDUMD3l#gA zAgkqy$z;Oa{T=`C|MGn53+qEu{;>h`Sfs#A(evgFv&IeU~PoAPg9AeC-P@(pN=O)-`uhm~4msK(H75*IPhb8Vg-MuqJYfRn z_ugsc#RuzQa&jug99A9tGd-hHq z*V~P7uY~e(n784=Z|Mg~Y%7XFEVC=4-y{EBQNr?;oH$4dQW=>S=|^UyBEb{J!(|;P;{c zzUHfc7S<}p<1y38bN~}(vni2Md_h4u5Ou5XH;E_X=;>;na<8ZK-1Oi*KP=CI(E(7 ze!{jf-7s7;Nut45-QRqOh*B13Q-pBLgg91!d2|lu^9gNxtYjHu`WX~XU9YVU)NReI zpVN>eDUK!_^&&Y5##q+t731-E*cV!Bb`KBCFE09hBLn6y0&IM9kbi*N%9HJVMC`O8KsOyGmLQvwzW=r+OLO;$t+RSQy`;=ib7L@MzJx`^dibRV5(=+wF?BZOHREX_^C?x}Van;+%9eB1s}*DX=mb?7U0d1!)?y zS*@|U#aPYtlQ*b(F(n}cv6KiJY7Mb;JsMS2flyds(b}E)w%l{|>7bdd+03ZNKL_t(!G7Fx$WipxfKBb{7TfB_8r;-_C*gf1~ zqlhfa`-X2fyfliAb45Z>>GX99#(5iiYUlh|0c$Ob>6FcW&l}I4g3z3uT@Ue_?cYlU zzRQ2UiyeG*+R^2tjF`>leZ>R1wx+GhKK^pCyb^A$#*+zUSuh??hPH^^c8f8MQ}TNH zoWJomzBg=O{}mYF6%>4_N;v+VHC#S@HaMobF6g?5&HV>h6*He-qm-A+v}WML|t{L*dcJQKlo|lWQ5igt+C7( zmxB>HlLrO;d^+vMF_!ccF}WEEeKGN=^L+WKvR|lrg6O3qA7X) z{9PU%9xzSAXf$FxpY-qFVok&L;em8KAA(Y)zzwAA11_L7kLRvz*DI{Gj3zTiqqI+; z8#fWRX@=+IleY0Vr)^zQxLn@z^zw@Lzwk5UvvcQ^w<~`C@BTL0w!w%R+bNnp=mUtx z6S{7TQfdFb8CCVbbb5W{m-k4rv4%)SG);{Kio*l*#Wii?wA$;Y9s;as-*DNrEp6)@ z!~Sr9zQ?NTx?zSzT{pCCOHsOM(fj>@x~}>0AOGi+eef3|)ThC27;K%Mb^okSD{E3p zzW(dJmd||V)BKrl|BhiVg+ZT3o=)YA{T}p{mT;QA53GG~5hBZBsXatzeXkzJ4xbOY*4l5&JHN+=C;pZJ~*khE?+n8|O!)p&KI$>h)(Mr>?4m4RjLI@_43E%XM-++o0fAzb*8-VjOS8acNbNeV5 z@%uAedmjjX<~%+y?b$SF(l(EQm9x_l@(3XFr7`=tT%l! zmN*(qnh;4z@$f)hKJd<${A{rBQ$O*Kc>7bIp*mD#ql~Jo(bbO1Iu*8dze9)|HFiA! z(pvuVU;TcpI5v9w`|^9y_KnNFIsWzQ7f30YOcq${CaZfCyz4s3a!X_*8q=Tf3;9xq6eBc0msn>3BFRGc7~h16!DWk`IE!*^x%5fHsF0sXxsfOv;jg0w-&n{QKS(f9f0GfPoEMim`u;` z^jAOPK=)L358`=0=aLX7zudN#v$G47O8Zd{(g}{#HBGU_8bOrhOeT}ThKAL0|DI)8 zvU|AY@Bi>WIwm9E{LR0jPc&+r+Eq(rEoJ3sCA)_^+&xROETt+7ryA8Xh$P|3lQ)Nl z#Boa3`Ld%6Sg` zAjkXt9pf}(c7BbNu36r12JaY+F@xe&*LB>!e2L1kL04a?bU>0et6Q)wXIIaNrDd~s zeKu`VljRv*RUo5?;!vYf*M#S)A{t|yT8)=KcFw0`yS+yV*RXbR@pN#qr%3ZDt-?F+ zkV+8-6F#{JHR}EEc*7kY3h#iw;F8stI7Lx#*lm%3wH;|X8YbRbB8tNX z6)9R;gmDtoQo7Q6*SQJkY3h{4($vxQ975P&b<4hNxwyD|6ioRhdG8GUvv9-BB9`~} zL|BTZ;n}mdhQK;hJn=l-EQ?VpqN-d&q3fS&iG`!|MG8b5)08D?k|2{2N-6g1JDR%T z_x+y#h_;O=>J70NAxC3k(~(Rv>ZT;m&JhAckCb*@N1o+$+Fy5#AayfT`nA?whmaj{ zJaz$G*Eyvz?EXo#_&pnz^!@%0 z5$IDY*HHQ&8UfwwG`S9^=7!hl&w>w@L;8}pbKyLNkj~@JV)(iKcf!CwOt8X#`y@G} zF9DC%EwmHiwJN3fmVfJ4^PS)I-H(!(FrfDPJM2#hX?6bVz$0-2f1W&o@8371QH24k zw@rRc{pW{{+yB0A5DzOtVc-z{ynn_YhgSOUhxZ)bpQlgx27I5m`t=Eum(Vtc>-DSu z^=}4XGMy49F-el})?070ym!5oyWN(}cFXW;^K(Z> z>Z?14!vWbR&9RC>fYxp*LHNe zxHzBd(2$0gGrvcI)j#Z{pY`5XMN_az|N~A3(>+n7Z15bv+u{bV`!sk8Fxtj|$&3omE(r-`jt1c=r(bq_ zTi96HKZYvikVXFWD4wdIP!)uOx9&URpRADrK6!!A=6~56dkka$ZJ6WA9)EZ^V}0|^ zyVMFiO+0tQ&g@XJwXq>+w~sQ@TDL{Q!hT%#LHAYlTwcMNI?$Yv_-oBY2l7Oo=*X2@ zie7t`vLKLu!0Zelf>;eNf7b57{Si5ZnRRtTcr8DEm z>B~!f^w@A^Vv2#rsexOWTGx7u$lda_+LITafoj8LsjM%7fl*{AyG$2$`IY9rw2zEW1KP#XBw0HMOh<@UNewSiM-v8aMBLoS3rR25K&ILkb`SG8TU< zcq>e@fex)z-_|sL1lelU31^3-Xwdm%WD42teK#Ex zxZ@^4WD!g9NeY6-`*XxF5g%2&!4!`p(qZ4;E(AL#p(Dc7vd`T0Zb*aSX~xmZqSnRw z`cqcC!GQ+IHRxp#lSUcu+<%U?4x5!wP2m%N{lp!Au$F4O80mz#^fY9~V~=6LaP)4L zOqwGo>+4>`M~~oF-;us|zxe_G#vTqqG)o)A3{x|^1Rn{get9MVK(jlcS#FO zjBtVoX|TS|ziDn8oUfI8OrsQ1-G>`!QbW0!IH}QUp$VrB#UHzTq%y7H6p=7Qr$&O=<+kS)(X(tp!wP0Q9|>A*AEyyj==!3xQ{7(U3H_DH#CS*M?SI6It5yNBV*Cl9ShnnmQNAA&ncRYsW-bY zA%Pr|%anR#YNH-u(yh?s8I@k^fOy`39wuI}SRRw`MswQQyVt)&;C2YSiZ`P_u-}ed zy&_QaZBbvM+grUP>mn#ga0}xNpR^Ux63-$f>Tq?_oSu_G-NQ$u0AJP9PYpH(z)e_3 z^+d}7&haGQ5G!q5a_;$p2Ss8Eq?Hz+DTn~LT1H+6Hai;IMZ!Y2)<#-5w>;-exGGg~ z@!*Ky1-6O*(A$q3*ogrKw*ZeG$h{=zYeZVvU%U%~81hxhPzg;C$)&`y?^%t~qCPjNV6t^<%+d z7{SkK>>dQF3rj~fIzA1NqgH$w+p~Lbw_uz_;8D{f>GPDy-MBnjvzm3!XN;gODqUGj zxFT_VID7#?o^yM*^Q1?8nm@IcTbuf#pJRJ#uW|cF?U(cO%Zg`Mw^Fkg3Xd_>+uD2a z0UoCBn3APk4A|x##@|xpNf10LcUxVTY5zNcYZn|Bh?G{m-Np=0{KK!TRm#nw(!mMr zlT`;4A4b5Mj913j-Dc$=Sq-jiE7U{`$>7Ow{}lJ2ATp*AF^v^B?N1eCw=CE3yixSQ}n&g zm}s?rsivV&!A<;G@!{M*CT99~<*di@!?7qQg@;YItvYiu;B}0n)Sg+sza9D1cm9l$ z`({{xKJHUk2^puJU$ivJTTSOM6Y2d#Y~c%2(H=(BCrzwb^NP5N#+hP z-(u?EG1LNSEL_sAm0W0qFop1_lpLVXpX}N#VrtK7I;JUPXc8K-=ZMlmjouWNT^~03 z70fyt_=9pAko2b<{~R8UFvs&^y8e7mD4V3!zqA|}`8{dx&|E;xmAr0Mmx1@bTPWUa%Rz zb)zx9$fHwiIMIeK%__Sh0U&T2A&&D z@}w54v6ueJpCRZ0#9DBjdzf6fYq**i)HYH%?x=_Gj13hBn%}w# z)%Bg9&%TqhgPxW6aqTN90Z==Fd;yg8OkbKUtYJ$}N#oQZI-bPc}=(*R_>=z^&f_x3p0Qu=g0-&5!F{*#c zAhM876Yb7>v8#CVs1-VJCib81c^t52x;yQEu*FFB@7p-c8mA%};Au)8zElT_)1=SKwUYm{Komv9D+Fo*)y4G2eiAO%m;f zo#EtSCK5AF9uKAI1{sBsp3-643KDqEbyOt+7;ntkkh=J?WY`Po;%IOPzgxDy+`+Gg z-9H9Ee{u_z(6p}E-&9-jWUC)y35fdf|JuQR$`zL|)~;`*)j^;3u)K>@b!ei@mQ8z8 zq!tO?fGNcMbOPf^+p&SOXslLp(1?_EW>E-OvV{m%h=7P`C zzpj3|(R&lMnZ=(YzS$p9MfnCMmJXmkXHq>lWJzQvB95}hBUgi_sKf9 zaK)D}%=PC$)#RSjOtl>w0pwGGkBsFhKpSl;1F$7I(U`~7B!!6w{m|?8zq9Ge`uO;V zNP(;O?lNR@1H)m7*UD}nu!}K?(1F)S%%;nP8W6UWQnwFBF!O;RCoh;wRxS=rgOJ|9 zDU>~L~ zzgoeok%YnXLG4FHSqRV~2kc!H2b3A`<<|-1haFGE0VnQbN3NZHb$F{C6BN@YOFflW zkzT>hmbCZsFs8MV#3p1;U^`}R5bf-9=dCS&R?ojlhL&w>Y!2=3pJ3$i7qBwxue8-d zBksqayQJPV>6AhfdVa=A&{sw*zCJ*-_>2GQfByp^9GiheUJg?T+xkCOI=uiEG(p__|upD4FhzwqD=CrhU4;xN8@v z_}LKHICcgvYXjyzlsJajwg1f0jD&k#>9BLP(IWMz%NEtckF0@>;Du#m=ITeE( z8pojZ-Jy&;eo*@B(RKdjqJdvZ*3k$>UG%#^gv%W!q!m!ly61$Us$42~o(RlHr~pjL zSBr~tjswatL-@S;@{eD`>Bi`@_zwO_=jgUsY@|EUL?c<}5%#l2*z6-w&}F;@q#xqB zuI@{c8|zS3Wj`-J$@eA|wm)6Z?!%pG1J9wh>N|OO3i|J>%YEVM+BGZtZ(Fj#F{j0F zp6`xLl|)K%N&hQoUsn>lp!{)^8g$kIWE?#d1mD5?nME~F)%FCYzC8No(CAVN9mBuN zpNg7O!IXlI#hg{;6$*WonccHY)kx5pKRw%|XvWCVLi4<3GVTRVf13zPkSqP~kF02a z`TgL(dh_y8nufqFt792eJL9nq=qum|&u3tQZ|*WV-%|_VOw&z^<0>$>q;thfJHEiTxBsC&FMRG8HM52ybl>p7LMy60WD)37bd02Ja#!N#CeI|TmA^lwPxH_xg{T_Vt_1R`1mnJ@-dGpB zZCG0BvJSk1>{>*K_INf~aS4;XMlj`7HQLs)4iK9xL)@hy z#&s-SJXXJx`B$i;miizkKVt0`SWjv+?ro{=>()R7@hbnodF1ZqHymH-0jLld#2j;k zLoUnl_?ZP7vb-Ozz&ynx?M3l{%=NEWb#uF1~Ds5x|{RqQ9A7uok&05FH^MhIQ<&g73z{U&K zuilrkc}E^8DB&al`600bn@^tlG9g%o*1|sj-hJh?R4u^X+axDTPRca=MLzEOui#G; zG$s9cBQZZDY7w@1-K}_geHu*#a?CMA(C&Bp(WFTME1n0a< zg|ElE0rjTLg^jFs>>(`>HEnUQ!2X5nyEA6h}JP3aBND<&=ljS9}S52>R9eZx1WHjj-@pymf_jyOFK!;9*Qqw{8Bo z>o^Oc^}WrFgHu(T6@T(wXo9)IYwC}58gzL~cq8~K-jOM9-zU-M|NI*EpOOX*2yN@t ztD-23_OCQ}!de-90_@cXa}|CkcAd^m4ppAx84)$xix1?&hC)I&Fqf03qmH>Oi#R~K zI(xC%eE9a-US|B1DS)!4er_t~x|zvBh;67gtIU27X^dDjPw7?E(F3eVFjuSZ>4_zY z&ZeOi#YLl+7mbJQi?_ot13pcK;NgvJg^znsd{OZMMbpR6=E|22OA-|gNZ(6o`7(Eo zun_b0#-q#2N-=1Qt4Ruror4C0fe&BLq4nu_vZdq_#rVY1+S-uisD5*BKsl4OJF&(m zmDMJQacqS!Lv;TaOj%Ol3C}Rv<#kd{R#w7yWs0C3WV2+#AefD`B;6|xmFcqwpA&iH zJ&2ImFHtLmS!DX3=t)^#x)4o~E63wiP1dT2Fu@|GFjAb9kpfzaC>r3lw&2Q`Y$R}M zC{C})0KIW_tZro7oe*X`Fum}G=K!|^ft+x8GS({9!)8#*%R1xB2X^T00}>Dud-Td0 z3E#?FgFk2F{3{03DpGW!SRL3o7{AgS^eFGC1FQv)bL+z+%&D|Km; z)d9V}4gb!3;#+@yWg=KVTU#w`ecGMxlEWKZT4A>n&cN1VCH!Bo%E2#i{pGp;tj*Yy zp~ox2OZd~pLX3s}dA`X@lkJaG5v7{(IwN)~2FDgt1CaC|0#a9Dkl6t7h{dqK7+E)l z$|@qa8)I2bK1(4abLY*hUFekHE$QsAQQjT@K3n62h15Pq^KmQZ*#vdlQe}Rs z$wuhsoPD~eum;FoCZS*b`&))-L6quCV#^YKQuQ1A^j@WDWOc_T!?*}cDy$-_w1eXS z>Q`UO_Bm#>it&L3Hi#gU*;!dzdyQ0RRKo6D+4_@m3SNo)x)+a%v~XmkktcEv48}2l z?;|aigDZO`Y#fP9J%NrSooe~_F!C^spr-(1kMX-klB0x5ku z*=pFEq0mmzN91Sh`rUIdoeC#dE)%MmK{r9+T8`83QH@;b20Z?6{jf1rAgUBWn&7~f zVOj4HO)8}Euw!HW>qlZ7yXKmW#K{t`hja+r_C&;3xBy{hGJl+1jb&Ycgs=)1823Be zyPeO&cvINy6J&)ur;2mnbeJ)xJ-Hx)*tm~#>s$LheKQ0tJ?tug;2KfGCkKK7y(i3*~z8@1@>*r zNmf8~hbm@it@C8zh?4#%b!C zmivZCV_v=orw%!a&;oQN8U7@M2uAXcm_kV&*Jnjv#}|?}cHUb|1>>#fr&Nk_WDpkE zsjhk<1+WsfW^k1#EU`SaTKfdV&iQIhq5l5xJ|Jz@jS1`ZhjXzWBJb7QF=9$;y@&^& zsHxAMDzEXF+n+Ap`=d%xuSFo+J)CCJsn}91Eg?e_hxUZ5qC8^Uc@+z(JLBH@T%3N5 z6Y01ppKtnG^t&v5@A6L_*1VM8*~D(Rj<<(>=?}KXHz&TeA~ucfgapOClqW*1zvKIF zBU`JQ+x(*DB!PZEeveBJc|P^wfA_zrnTejA1tfYT+gHW$xfN`z!|9kgq;uW)w5CEA z(~#9p+FsoS7uj*jRhk$cD->Vk+Fk=aXO>*lD)?A{7eY!3;Xxtikf&$C0dZ2A^#DG5 zOVy4%{x~gw=aT5oFrfn~E{l ztTw2)#Shw7yTE@rKC$4;ac9j@_(bwA=dC-w_XF1mO9<+FRWqNk9~0(0&tYXC`f|Yw^2tmFo7gZIvEa zkst58+%-q6W<>7_c~?y|ABKO%nh5X7JCN~xN^&E>no=U2eo!=9{|{4P{p6maE0%Ti zHlynX`xg7TV>SLFjy1W`7%&I}Qtihfu71l}8gNqXg;8w3fhCPE3#EehZfh`CFk=+{ zei6fvwC@RcL+|gUVr*!?F(M9vUnrC^zPp&Z2a)snu(udTr=ni}bE-xOIjjGTls2Lf z;uM_$f*6lNgbK^HVuq%|Dw}K>-xDQb*!mr0j1iF|!s)J4wt|P~6T1Bad!J7w=08~1 zwO|AXouQ2qQAL~QcreW-SiQ>|a`SF3)-ygMdYLlWu6n$D8OnLY4??$+a^ualBW(;t z%hJJRh@KRpYUN&cOpy8qDN{&FuRLIeqDlQ$_3`zNZK8<2;`Pb22#D9Pu{&ad+TL$V z88ezUAvQz{ToqF+Eg~uo46q(B0juP>_fHo?h6@5+mPEM6+$09NM&&_CTbziRx9>x^ zgm!CdC;d3Oe$ZEs->4Hfpd$8n))p9*2i}3c+QKJ-!!>TcOvaFdS}K7>gqRAwm>~k|L7tdmT(E2tUsW=Z z?XSox03f@-v0A`k1iJ66YK-FRO9WDVIM3M35`5F7_`&2V77&a*vg^33Jr$Bc!WQd*GibG84e{iM0$DRR0r{{%q| ze}_TD3w@9FO(R8>-es1qvWWXGJuL>vJy(yLbx=vrMKnwiBs>^f7@j@-C)eL2q02H# z>*3LNr70n2kPA^xik6k&h!n{t4JmW{X%ms9kPgC)uM!{3)i1BQyPQ^A9kv;-*O5OB zZ$(al8@G7<-c&keeneE=w2pF9_g%f*-n<9|ZBjkbre4rrY-QfH6IcG$FXWn7`zXwJ zjzb3JcM&5Kxv4dqSXG}FEfR;WDsug9kgA$deSCXHv2f^nxbk!ee|90_rtR8VHS09M z8~m*ytksQVUPz+OCeARFJZ!l{jE!A|EU7->1NW^dy1_Ld1FR`_VXqq&L`-AMl0YeA zfQ?t5z3QyfqD^$lcjKEh`2Nhus=CeU>f z@iGJ|>}|o~B5F0fIPGsvie=ML z16NBcfe=nh`v~4GxvHd7{ob7bZ*QE-0x#ulmUY;*D6yK4KpyX9Y(LVp-T0aMdJHnd z>h%USKBI(;_R-z$=1F1uDJJR&^|*e$>c9zAWi&a@)Nl06b*{fe-p>h|=HGi-QN_RL z=H#4c=oFN6fUU&)I87sI&bGS|uO@LpED9TFnZ<8sD%Pug#vDC_6(7*pB#jW80)u`n zImOe#_g{JzQFbUa_$)ZH&0lR9Oj(AIK;ff3=bCuB$GKZazu+10xe|dQRyC$Ku35=e zHUx3%-Q;{OrvnL$oS%OLtFb$5DNzM|D|6-;$j|x{z}rEETLR9kMTpk6cJ^Ok)o&Yi z-MTL=1$G~=fBGD_8vmSGa63V{8%IFpe_B|RT#&LM{-E|OU7nm#)@TR50Oj+RY93b z+g+ma$YqHwQyQi{c@rnST?!^n{FV|9O+r|6aZpjs1nOz<*W2@1`IUbc3|PEbnFx!N=H{Jd`Z&=)_y&>M=2yCfzqX~_qfOtx@mH#HCe zvD4SoqO(x@<*1eGp5!J%wg~KcOop5Z9$s)#+?#BvL~D|OAj*UrRiR8+T;oN7hDZr3 zuNKm(A+v&}M#zUzB#jhxZGuJz+g3`IP@Qnrkk7@oi1paS_5Ds-VEu+p@vGvkfh#CC zw#btaBb-TP7YoxhG0t_SF?Aqhjv{EQ>d8a382_lW-~?D^@TwRmt1tjeAFnwuPDDb8 zc8socNQ+M^BcHkNPRCaPQ5tgYj6Y1Hq6UoKAIn000*mrJjU$c>%QjyXqMW^U+Vxde z#4AYqC~n;r@K0}^PSGs8Fs+C%x<5pK5Ibr5I1$WQY^VB1S63RhYC?{>cf$sm_dk;w zPJq^#W$|#CIxc8kM=(&(yYu~)z*=frfDl^5cT3H`+X|FQ%ja1b?CiBd+uE@A{7^VS z{~Vhzr)S56nd%NFk#`o&*nkvvNCTUc_&bQQE0u0#u~4?c2KfPL(I{C8bU{1_C~9WS zS@%#cvt0Wl-`njU&B-}eCdoXip)GHY|B13udD~%8XMB}sES!5pso0uH;CrpNIAph- zO5~$Z>ZoC>vl5K;G*m&!c%DZE41<@HVY-qhThY9wX!Vuc*CCPRQ_quLswjgNEv<{P zR#k%Q9DgonS;XE@&W~c`xKlOY_sWk!)qNgs^)tST@tSN zL_|dZ)wP4`QYC$X$F_o_UYUWLHTB3d`VapHiNqcJ{Y}~rg!&75EF8ZLI}P>*SBha=nyA1=vs^i|Coj3&aotLButzt6@C|;Wx{xGuiG$!<>6_yxb7?I; zg09=>u1BmjASQI`-6c%n);Y@VE(8I0P;VdZ#VoJe@|DEwml+Q=@*MoN zFG&0MLucOPxI(K!DA+vZ4S(jFXbX4na^D$ea9C25NePN$wk_iu>uz??oO6K=HKsCo zHU`%tWEe{@JhJJAJdA-BSwP%Q`;4pTbNwK{eQ`Abu*Frk9#Ba7I!`7)_tIm=v-2fu60$9ta#4%G zJvM2MdkK=xN<$l=mn~p|nwdpO7HVdSq+uzyAacL=%QNea5@^s!t8SH2;^8LFHO%E1 z4NiN?BNTTA1jf#-MVaI;F9W`v#dX$sW7xsC4gc&uq;^Ko%?Yqzmh};rHRdj!fTz+c zo62=`^IsKApuL4gqa46rZ7++5^daB>rLW?n+tvVSXumxTXNo&B{&pm)7;Ua=JpX6j zYx9bru)eNIxfvFMev_p-^&~(E+cT8MbEt=Y8wnx3RuN$Gt2}o8d(^0mGKuPfF=FtD zi{ROm> z{YX}OcfS(X@0vt+=t-Y@6;Q-&3(GNyg04*HynKJzdHFfycHDUaOvt?oN!~nF?LiN3 z7`Xlyn}<%-{D%(uR!t?xevD<^inL-C1l-|T3CGKIz%*NRytq(~>-uaLP zw1JS@M*Tr9Tzx~(vrdI$@4%KQ?z$hQNIG-$kJKxY(=eg|Q@@csP$%~a+4f9q;F=!H zRoui_&inVc&$4#o%hF~!**}R2CMIo0;8dI~HE%JhUGC`Dfgyg7*`M5wiCnkxS7O5e>i++c@3^R(!U;@x-&dJX?vaCliag z+RBJG_xo8qKXBy3myaVSrv5D@Qrq8A*kw*O_+>?Go$qJYsZJP1=t3)(lE>!OLiLML zM`SemHh;|bCFbgZn#ZT_C@VhQw-(g^M4_>k+j<*#T6NBr@*I-~Ec;8PmChfZr+${& zJrGVeFTBtjE!%r%MxAN+;lz{7ydsKQ@U7}r+^++N`)3RxgLjJA+Iv`SeyIm(M287j zCz4EoTu5}>vS=R5+}YMkLDUM*jsdcG(f&>#?DtkIpy%o5jB+O1Ur1ZS8;-rc0rEae zp>uybaaR+xaBf+Z1bEr(K+{;mtwyxDR4#mE`MqQJR}X`pn* z5Q;GSD{B65DZy!iC%fkj@1ZScnZGHaP7QiMtOgdhk&pkvnU{VJ5TTS$9vx-(*Q;B< zqW%c<0lU+-?^vx6KZ{YuxBFa#pS(Pu6f5AXIt>m|1<7_h*lYR2qWK0+w=dv(571v* z7|8%D$Zx9rXJs4{f!TMfnq~$*6t6SEF#AK`-}z5Y--T@8Gq?DK7rcMybG(&h9?j5T@?feTW8%baS9vsDNeJ{Wk3+l=fr zQ_`o*pd(>WmC1c>{;5Y$=&@RVWsSVobNzt*Pgc(GhG}azvRPlL?OIRULOrkv(^u8>qhMy*Zyph; zukWhbSf$uyU&ZhNKdBY{?)8_C5k?gaO!e>i^@Y^uEwAOsdpbZUbDwnSU3E;IR07C_ zqzpp2ly}~dry*7Q-qJ=KtXFRx-$N~CPcI>G-+)*rtMm_}jF+m5|MIdwN?VV3l%QhZ z!qr?jT&0#%fyJW2^w0$`!ulpL%jx{5B+ETp+)+ z8-CCaP0*|(Ea#Ldb(DS}3tv<4>tS;c`&*CR&7=*Rn2j@yBm%>Eriw3%U+N6%SPZS} zVobT{-()hiI8uJb9o%rD*-a!%=&EB*8`77{FD29il0$k`^D?-$PT@V5B40_8>KmQH zEkE=$(7XKalS?T|aw$^IbMm{fWt`6}9+GtYi-)2w+Xvr`0}pwwluRI)S+j5|5Q zI0XUgQ%0rq&`lVI=8+^Vk?UqhCdeR{p=gF1 zBXDJb-)4y_{Za0q&uA_?Kyx~{IaTVqRLW{3v!4HyB$EQ#6r;)bs8E;UnLv6}ZY7pI zi9jvk)bW3VkqL#yWHl})6|pSl-f|6?<`M5G+KcG7(!kvpx4&wVfHPwNADQfkXTg>2 zsSYkZ?zqAFtY35uEa(1R5G7(bP@#CE{V41EaYI_JO|zDK^6~cd8E^D2qRl`}MOPt8 zW^A$zfx+EeS_*mPG=7UL{9Zv`O%wLE9?400W?qakCfN-kj`<3tw;y8 zuf&fk#lM09FcjXXuK+fWkEFHutnXT}W<%2!r8v!4eOJHVH};NW;R1+4Xt#&gI~ z@9WdlIK~Kct1;{~6Vkib{uNoWP?kz%{22@3{4`wGh@hCCpSLihki~rn03%YFtkf%a z?tK+Mc_~BRIN3x^W8h*5?J#RIYQemIg>qr%1n5vi?FJ2Og|q z!JPXYspU~6KaxrWZzn}-7mk&{o1HD0%L%w-r9fp z;zIP|E_1rHuLU5!x#|>VW7wpw>jquv4sKj>-3Y928xzn`IMl zfeXfqHQ9fT#lYIO8@K#y8iUbv>`4|kh-7ClF-Je{KsZ`Blu0kwY}QK~$GzBS?*0O0 z0?=BW6cZM%ZA*@n|Dyk^XaLob=<`vq3~&q);-ptP00iLMMg}Nt=6VsK?0~s1Ti~0K z;Est|s-%ankEvV1W7dO~TvR!qQq_>`8yMo~7o0O;jYkT2u5s}5Jq~$`1+kU6rgY?Y(~ytnuJ2RC208+o4)QHOL6OK79s zr=bg{eH)PFp? zBq!h+?QE?F!NYrSMe!sdj9Pj5-`>QlNE#I%A+rurtmeqEUWfWA@*kN8vur?xoWANB zikEip*^K(Ep*k8kFCDHcqY+ZlD6@&7&@7%7I%K6EXRoc&QtX~Odrw#x!ez)2s5}^# zulajWYSM!SC$WfG&1H4NIGP(}vv082gRQYNqrzE`5;jSh!bj%{^`amS1=>A^A*d-h zvYH_DvUeRf_1Qfv^5*008)jX=RlzZ$ImZjAD;*c24Sdp?H0(4oDtZ>aDrQ6($-Hp zT?ELl&6|q}#?;eEUD7Lbq~x=d6_Uc9Itv6vFQmK9BD_~9cZK-JJB%1;b}aLI`9yW1 z?kAR)mnR4+2Alndl~LV=b2$JTxnY`AA!!1lf1i}X?)YSNgk*!F{}H;@PHz6BFFWx` zo0o6OC>*;gY7;erL29Gt{@Ay9PV@HI6JCGR5(yc{ zx-spao;GxL0s4ZTP|G3NZ^$Eh&C-*H6h7%ow{?^_vg>|CF%YyU_wwdRBG#F8MHZzI zYF)9&3u{cnYDLZBA&>VHyWid9T)6qXc^{}nir&3^@Uc^ZL}Lu&nPQ|-%R_eFgLO_< z`tV)v6SUG{!=ggQfoBw3*J^>-is>`z2xTQDS zD_VvkRFi&Vok5p7Fl1w!3H#JS%URvrD(9zRy5CWB@FB^Pi(!h3>pf(yw!Ab>?XrV$ z+#*F0HPF2#q@L-oBc*!xRV;_eVx!f|BkR74ald~HjS6=jQ!bT|^BmH+dA3w$xlIku z*J(SrYuE344kD*;-rot)^~A{us7c+PI5V#_8Zm54wY8B(QGtWpgv7n-Cb;M3_tt4M zt6Y!RC&jZdm%!vIVE1_$0PlL9f5`-r4<{t%l4DI>Pfq^@p1uwGQV?LWosm|*^b`p9 z7uyMK6CP;hv%4<1cg+9YJi@4oOB?Al6c5O8Vg0ie$VrWZJ|%BQQX1Io zgh;ace(et(%$p<>EpU8skcUXJ*J79kTuu%W5#>@fN6>6C_N&2v%vUKWhme)2w8k;z z#6l(2)LayjAh+bu1xLOz6}m*a`s@a}8VpgP;?qicKHM=5R01Zt;!ne_4>6OR4EV)! zb=M7)6HLOI1TrcGRNAc!$%0NS(n?#$Xvu^|#!)nBPAr}XwmCnBX?oM$*32!T?z96ay!j?Y=A{T|eCbU2I3aNaJ3_ zA|3G@t}>uO$W`K=KZ?-r^a)<`r6uaWlEl>nZ(^SUN8Nk?;|$WsmsB?_yg(_*67clv z9pr#T@s2_}vd@9HR^<_Z8;yI9=h7*94!w#eD!9x^fWk`v!`Oo+4p?d`K#=9ug`&N< z+&4OY`wf~0e5*@sT{5fKxZgVZBFf$w>OP|)j-2X@I^lPJP1n97zzE%R?KMHcX(`q; zl@-dhGu-=)Y5Z&YIcnTT=Kx_MU5bB_oGZQ2T?!LWoE(B-q?9dB(}I&UQw_Hd3}d3u z@M6;rsF4SDisjE)3y;_??u-(=7#YkJ$=av&d^H%NEPUS?ykR!m(*a)o$9KU@W5XBb zDRZ6VncNH%^N<9Y#<|)cU!~=nKA{2PZ|)&^|N66vJ^+y`fZ|>}9W^*O|0g;f;>NAX zyDIklp$0a2Lr@Cl+)E+)2(XINldnLSpb(ze=Ab9za;FK!{H(1p1O-*#qqzi(V00Fp z0F7VpXKL>5cbdY&GF6^p%^0dZJ5f-72&;e0O34OHxq+uj7kxBSc>zM6HU`W#wow7Rs;y0p4FJso?)BvnA9 zk}_FO0Hy=QSZZ=)D~7lUkb|Q^R=@33kn4|p2L4RBFNr3}a%v;K_}$<+^gqZO96QT= z$yZ3~+Sy@DY3NH91bU3;cpE*Cpnv0K!!@BIqkSUt1t3}Ksd%R`H#UYQ>gbjfDIzuK z`}ucs*Y#JybR!B3rH~+l_?vb2#)7>C@-sJUYC2>q=HoG^mO-IWM~`oF(4$SH1&FHl zcWGRX_r+l76xwH>_M}(PPP=sF^IRm{a9`2r{-n;P6vW%Im9ve0OmNbq$J0au?}-_K>ujAwMlUQ{kR`An7JxJP0&p z?Ms&=)Me&{Jbr_*?Y7WG0y^BJ(E9eawMhH`3GChFvcafXmuw5AlV*E}h)z~3j4lJC z25kGi1AAs8;lI?-rA{j`lWL>NvvIe-6ijb}Ls(hDoy$q=%zun=8yhBCf08LP{+Ieq z)d*J%=8k1Z^^FT83w-$`aPWyx(J!*88I1Xc!E_Kw^RXQQ3XHppVhM2FP@nR?+0EyS zBB5OTP)_%g9>tI%9b%YXVQT zrFMWKB;?h~?*7JgNNfkijMBc&$5hBz6O^3m_vIR!Wt77H?^~5m+!?F_+1J4JU|2C} zAVlzr7^{c1k=gD?`0$23vRW@teW>I8Q88oOBdk*0cvPgwI~za7Y|z#piRtbxY)1j5059&3kBV^o42+YbeX@o@JE9@bqFML1H2VNHUWD zedRf1++Uzx$|1}`spFaz?^;}iRYGURl@b5I0WvfT5)tnps7juuL}3X*5K&p6yobC1 zi42$3=hptAD)~X|yU%}L@|EvSQTvDZJI^o!|H%hh`op`e(W;)d`MZYWABW;kZ*PaL z-Y0+p1Hr4E9me@7eM+mk5GR(iv1^2pO09c?oQu4W<};jKL$T%9LRX};_@2PPORU9E&D*f+_@Q|Aeyau2&0U;i0R+3 zmY6PFME}hYOZ>WYz(6fiaiT5giNt{Ue1YI$3@I@YKFTpPj!YlK%`CeXO@3xSREjZ33`S7`D( z)|+9HtHjQFIfuEJ7K^s8dq3u<Tp)iTBSn-?Tk$ajfu z^>k?B7642;_%L_C(ujS->f#&`Hff<8;!eq}p!s$T;@}5@HabLlg!I?>G|Q9p&W@7FpI9Ge+xA2wpkXoOEvW)t?BvCg31(Q*~e!6IfJIvL?Wz<_?#oRW&_bfsF4 zmEATVX)UaepZrUR9{Ld~9rZzLTttK}K)0of8d3-^SZfX_0A5^b(>F{9>I`NrpK0BN zpD7^CPKK0JCKnsWwAb*PT^q~51in7liL=ts9A# zG||;ps6!CbpSz2OrN)z^gTeZPHfG5auYgYD#)=75G%Guam5#cfmn9PP_2FTA5MK54 zCG`>oM!KwTW?d+mrU*XoRkN6-G~m9zjl0{2(y^v%m1Ey;kV<=ZDx+B(c#1DUF_vhDl( zaCL=meLPEK6|zwkPKH1QC!E zOMc~|Xs>ad3SI-|lr({cfIyjiz^)K)7eW3WlG8I$p=ti_u$^koM&Vf$c%iB*r!9%M z)2K53^9eo?R^5X6^W`&-u0NwTFJSir?H}mxh&l;g>!BpVlt!-B?_iZ)M#~-K$EA=w ztEW4S80t~H`ywi%lPyYT{`f!8kk&HGRrB3NC~MAI2X&6>Ag{=WBFcCobMef3_X4wt{QRfk9yjZafdv8`F!;T;b$^1}$VW(JPvqu*>IliDsxZ$bXfk zK3$V+v7T9mcvla*Z|K$e_;)jpIyTachLj}z@i#zhejj9ty@8q|>%)HYn|$qSUjyK> zP`LyU=d&3fef%*^V+rPtn}-LSYjD;P4Tj@EPCDerL4hvJdWV@8=NwI0(l#a6FFzjF zanm$R(v-h_S&aO)-}aj&Fe|uy`K9qg0fWA`n5?yJJD{l1Q02(5Tj7eD!$_S^E zAn)OehGaNY;_rA}01U%bpMU;cBBkgDlsFuE=inNR|J^hVhs_fXOPVi9lJn4Q*i1>1 z@c8f=`X1tpBuSZ0r|kD)nj?pm@IT-wYa_jv$iM%Wzycbm(rNMi}^V=_o;ux(BI*C~>uc_-ZiW&yL0e~hC-$7+EO16 zBzZztwKQGF2OqqkbD_I+rn}j`lUX9A!FpQTa@-ssg0^d!ObUw0oMX8g0(L$Y;O7L> zaX6HeLMB)>&Uf*JExc(4^Xyr4@ckX++7lNh=-}cq3U&Z^BySo=CB|K4aQH9`q z>zo9#-R_yDsgc1@Xj>_^pWb|%Q`hlhKlbB8fH`qG9dS)dJ}-Fv`hl-}<@?^D5nl3D zF9+Jnh~}_g5QoDnYg5 zjNM^NvG|a~=9#Oz4{^?7tqkr5NApmyWpRx4jx-h-;O~C=9p~X+D4!21 zMKSyRJ=$r!?{PXJO{YYWrt4(3QM-mDky(Obv0#$MG`$-@n=KII+H%+%8l%B^M?PDQ z9U|wPaMRlc=Q`4&AW4b=-L(V#(J+}L<6v!Fi>ad0BC~6Z7{R@H^(n<{O_*uBq;N&M}>?Fh=G~gobgRPYi!wcKoevdHeb;W*D4bF0Y891WW{j zNm`Vo;LgtI-uF~hBg$a5XLa)dS)On_9&oKApU%-khqXSPIMpYNg2^z?A$dXQ*b8VA zMG_;O>+$}qGWhuThCms+xqdkq6Gj87Uyo;aG$=%6nXCy>rvlgay#D+XqHKcpLIXav z6~-IVG+{EALF#ZH51R+Tv$%SJ7xYMCqf*3aG$?sSG*!j!@XUO^qCB3!HbhazY%xa} zL!ibAaD3qSwBf~zIiG(1Ksrf@loV5w`PEpghxHx`Xz}|hnNF9#CN|4?o)7>Ip(CH~ zwR1hY%@z}%=~LJ9l(N&~S!=BwJLn-+_dQhA9%aRf!8u1Zos9~ifz4rUGn|VP;#hJ} zSswx@9QF@f-+VlFJ0y-tcU)*ip(8uY@75JrtT~)YiekxRG8w*WH{9bO9^%;To-j_M zv=e1M56TPee==DN;N&xTlw-AHI$gd~@LuL$fmQTNzw}EZUGOrOyWA`9vsT`pDRIv6 z3%~FSeD$ke9YN&qy31lD*4zP#h55mRZCSJnNFvJq@oOwzj^aH(=5l?9v{W{-r}L879{W!cj&TVKJB=4Og z)@Stf@%}Z=Thd}760QDR0g3{W1gNI(nWX7h12Q)MzC}3Qg(uE}Ha?yoiAru^!)p6Lsp&GvA4PJIaE+s?XN7OHAI`4Ns z;@A*m7eo6js7PJ5`l2Y}u-lL&u}~I@8AUM{1E@5m=|sZNduTc_C7etIEv)N?C^CbJ zOG&IW$GRQvho;f`WIPDKTxP-ATZ(i?HeYG z6`&ELCXw+C16N}KVkD_J7hmxD^=B*=>yaiSg;O-peUjnu61w93{adtF4*b~Od*b1_pM3HQIHxft z;&`n2O~2_!hdCHQ#80OhV_>^|=6k>Q2l@Qa&`B8 zoc4S2Nixt$g<_QF8FkqbCovu+;ty>tvnYeo&*S60C_8l>T`%tOy%Iz%pp2nIDkyA^ zZE8B_xV`&mM99@pP=$5g_fjBwZ(t}!>$0S;kC?PzHd|p`kL|^8Nn&3#6sD5scU=o; zd{^QC*LA2U!U5V@x(?R2FGtF3cvRJi{qC9h;)*;^#(4?h`X^LX@FoD~q~(7)Rb-j0 z_2c#thhjdc9!YFdgo4>yp-3gh;FPG}?e}}ya)(C>tG)=q^+-3%^K=|w4hQ%pwIEG%j1d|WO0hp4*+0EuwthJj9o_hOkH_QSP0w&Ankdy452ts_G56n^GC zwC#x~lHXmHRISgX~Ny6zFmjm7ru;7niOx{j*a(=;uebvWeQ_~;|y z$Or#1Yb|fye8!i)^nHB(`FEI3m*aOz;&|+&1aKq>1Y9x+q-*+26iIihD3)Uq8Gs=O zGc_`wb~v19$}M8tECc6JR-ko?42H+Vu^9`1aBj<-oY0w%+Z}Nh!;m-0nzus-Qo7P+ zaCCoqd?ZR!Y}XK_Ig>1tYuR_~PAAYQ*H=qSBn4vAbSU59`j%q8W;&Um<6s6UMU&9# zHQVhI-Kk=_x*Fq%(Eh|o(3rEpO;{%(*HKDI2aujPFNW8q9QXHV1T^y|Vm@Dw9jD+8 z6z;*((<8IQkj_^$O*_O0*^5asAlVONV+@<82VB>1_2Q#<0I8r7CbOsl)JV+i zIUF`*5qPJG@`2iQJmM9kX-s<(IdQ8LlgWJW_~}PLUOtlGuf79fU*?4&&kpYy&X7=qz7J}L z*Z$BC{Tm`U(pv$vng(lwnekh)qQHBNQW1460KYNmu(s_$71!gvEstAbC8G~fK4rC9 z)Av1p|L^~Ub20K0Kk*0Tpxxdh?#YhT>Wbi$>72AzC7IGpr{{nbs~)Qo`o8A*xx=^v zCQ_uuk|N8{k*2C!(!|hP0827(u^-;P=H~7t9%zSPP?im|Ndn%ZBTd^Y0u?~`pVGRu zwiRj9Jg2Ez7V{b9>BwSzJJQJlVi<=1E`23}VNoy-T1r2u<*cab^A_%a4G3`S>=Y$a@oxZ3DWyCu26u5gfuLg%jaFb z=ld0O*vw(UUa!~mPB`CTV+CDsYdgHRL?*_gr5%o#6@L2pC*=8@BuU00&Ew%noJ0ei zM?6HHp5EepPZGuC=?tQj!(q!}aYbEM2sb}Svi8U@(=hil$Td1w!Jw=b)}i;B5O-%>SA|wH_2{Nl10(7P1w+b0Rz&r{CV_=1RDk)C|FN?wJgXMC>l+6jdwUx z&N1)74=W-vZ*>EHv0%Wc1PZrq)fqA)cI>^@`cM03oO8_bg6ec63N`(3PxE{^J=E1< zhltiC{OtPv72fM%&50n~520NSiyxe(Bpny5^~3tsSSt^E6p7z@5b((cHKA43QkFXu zLM3_i>N7@b?r5-ER0_767i?D#u=B_xPp7pqQu$<4ZfV;dt%a_0*l)-e*HhCfYflZ+ zsDbmKAetJAYFb;?T67%Hv=z!raT15;Odo44_2W0VY{@KCynO#H`PC8?hQ!7Fq^H@v zJfL()TUTVW8C~Barh$r3%Y@Td&+_Ve5}^0JI0J#8JCaCTQ#KM*nnfvEM;xmS^ZaI7 z6H*h2+QYzemx7POx4Cu0g<1WIG<;e-Jv3^8-Uy6DKKUEB4bP zQm2&A9u|uQ#@OLHg#%CAA>3`p=J6fHYRzJ?AoMg zTdJ<)>gs+vGsXgJq$p202YTEyG_zTX!co=L(5OD*bu^%ia{orVQEM%S{hnDC(X>6Y z`5c_*aM++xEN|{`j-g4H-z{gtr`DZgD4zt|1@yfo&k}a~9aVK;Hd_q>?7_k~NdYqi z=E;8ROovth4vQWNl(H`ID&gkl1~51m5yu+m7&zfVvG#s+&pL-;N)DG}yM1DLp&tlC z*~7+bc}X?C+=rcWyu3V+XnDHVS2uKZ#i^-St!_E)HuTmI#Uab(%``MPqrZx#>4*Cw zE=*#8Vc0%>7;={auJ7;h-k?-CQD$WD*5Uo9R@zEQv-!MvAR01nX0thAn2`+3^{(q^ zjfi}YhZnr_gxQ=tUvN6@(K;rHHE!5DljJ-^9Z{ppA~Dh$0;!ia58rZi_jbCLT~)zQ ztOIPfTV{Dg6ok~hqHU{B?4U2vs4_O{a%n~?_f3; z+OpQ3-q*CJo??E*d|ogHc8`Vx)>@KSOR(akFO;=aRR`+B0qbCKb@K^be!TBref8B~ zsBz71w;Pbgg1W9h8A|`~b@|!hQk(i|qjFjMU7o*k-5G|SXUqS~n)27b{xyB?2}7ne zG*a4yewI+zVi=2pvjj@;y9u2Er0o!{|oB=)f>Or zJo7*P%YTV;9`9wdd3gAKI8emaPU{HMcotVzQ#UmhR87-;Quyw6PlSQS_8q8z)%86| z7}6TUVZR4Ec$j;~ayh4IdXm|aBns(_ow{VXu`!YlAzWdQi?VxqB8~%WAj#@UGMiw> z@j#OsA;MAT7!DaXbZm;UtOr;Qs_IBp?avq0Iu=Wr%8n29B|m(;xtB;rq&3E!EltM< zdYlA}oA4!#YJ6QDHb3MA{_tzY1t=R!k5cD?vTR%OVY)Xis&NAuCqT}1+~2<)eBJfH z^$XAGsKe)RP7-jNwk1guf-vOq{kLMTY0Qu^Ernrc9P{OxS)PLONE0jWPcIK2$>uAZ z59r#OV9ICkEM^g1XK}XX)C7ZLS%1-#)q{HlpG7`2^fD?97U+$?8*@ZftXZSLwVQ?4OGN@eJ%77V+qtKP_+b6 zNEpbe?A_VfXFM2MQxb*=$LA-m?p`q>q~iwL_dVV72XtuBNkM%)kfs@%PF%>+L2vf- z-EaB)7k^CO8y??%&Fc0Id0r3%TyVc-9h7A=Ir&AASe>4BPqfN$clC<4ZD>r-dcK~X z>v1Yo*EPQH5K5j#>kwmVW{VZYe8q?F{%$Hx6&~BQ5JvR9=gU9*L!9er>xMupcH1M~ z_BiXv7E7+KuEc@HI$|AAHVtW-GS3q>`xDR4ABp2Rs0z6N03ZNKL_t)Xq9~^3=-hy6 zl=2hBK$=d`HH2ZrcJqksI(!gtcmH-;GnWUI{3$f3y56BkhQLBaF!@|$@6?d)eeUzmdhKmEX5f|J>gts1vgSc={)V8@G2kx`BR0g=5xG~6l&Adn6RK}YO-uLAqZ!>(TEsH!nL)2 z+867(MZ!q~p5A{$5QHqQZ;5n>y%_MEi!@oA!j+c7E^vl4O{sf}F)iEemNZJ~Oo??d z>-8Ls1{IKGG2V!KkMm|YZ#~{iBSRQVF4ExMHlpsAMdG*zjr+`a{v5YY1VKy?Se%7y zv7+2P6GbU;mQvO&i=rTmL*UF!A5lPbO10e(Wf|Ak*Avx>!Gc}ub73^zqw$={^PG`q z5AgExh;xqld`^<&1j=FTNFS2~Z!saT6vdh(icmTr3bf=+1i>WWzeHxX+b#1vCmNdJ zs;Vv!m(eBB(f8xoY#D|7`^_T&tLyt|j~a#Vew1;D@r82528bGT$MjLv9JPv7(U`kHqi9`Pz9(2C{q zYDmL|F=f3hkI)`yd^jM~diojb#e!;JZ=B6}{4i9GF{YJlp<=;&zM6)t;lo`?+!~3Xc%M+3yE)C1|{WW!64`u}+*0vlDFWlUS!HQ@s#C>!eGH9*w zfyY~+@QXEW$KmCLV!cM|7||We+8g!AAAWbERTEV4_}wa$s~8UXhJUk{`;DdaUEPzF<;%^@$mRW5Cr_$KmJE- zcZZ<{l7Z7`B_8?`9fX`ZSra;p$QK3G{(vVKsMHSUfWQl~y;w~F*Ak?;V>FIg-@f91 z`k((b`tJVKzba2B{`imoh`#S=8sW6cbWtXcSu!Wj=ggNHq)V;rqHa~sDA zP!t3hdrm69ys=>vv3c3xtfQ(r+S7r45Tji}Z@+JmTp}KqO~|FV=!c6)I`{L0$XYv1 zsmJDL{O@U+32*hVAy6SvoZ$Kftz`4|D7M>YcF#|=ZG-nct+BjppRiqnHJ<9U10_7# ztE(Gk#hm4Gg$@&n#hffo&{2%QpK_hO_Yxpgw?uIYS`kOGsGFw26VkLD){BFS=o*Aq z8-@X0UD4JDx~3Vz<}6_l&{?69I0sP>qLso}q2P3NNmJE?Nk*8YwB?SfJP?GVCdLs2 zG5Ne8O6Fv<8J>{iY0qN50yR1+K~!xvqi*_6#Ho2UJc}A<<&aGCIgyU3+7pLU$^G^HbPXQfeZ%VN9x)7v>5RuXPY{ME z=ZHd$>3gbE2|gf719352Uk{wv$2DI7o@2WO$y{uun@il*7^w=SmW1#92y~ z7j)Lr8IM;HVY);Ol#8ydhD?n__{#wtPbcy`90pVU)X>SWD`HH3PO^f<`ikB2M~t_m zgJrDHVggWVD5h(6yBD_GXZpUQZ49Sc=u|-vij$fSsjHgx)iqh1qC$mM0SYG~WoHM} z;5-p777LOj<+OcfwpdOD%O!mfARK5OGF3J-^$BnLVP6&gHr}G6gvIKLypZ=3#~EBS zG*W0uP;DH?G>0vrR)h-5(+;JBVVw%i)mnuymOK~RTd_wyql)|ehWd2iSRMu{imctT zJPxi;Jw>r1%VeF|#t>q2>7JcF;j7+p*uDNK&zD4da|NO0a(1FGo6S zhF1!7jE*9j;|AAvparpV!;mIPW~6CIU6)wf4{OCyRSjL&PS?tNk28k0+7gE;RS!`T zvszuTSgz=&CYz!55=w0l&FH$GyXysB#WYRHY?jiS(KO&pJv(Q^q-Y(Y!wlyXr_+`! zn@@5+<(_m3LqH9_X$1P=bq^>G#*A8~IIo5gdVnG;Z6pIyUp3^iPSvH$=I!O|< zTCI3^_zvgvK;Ih&RAR+!tqgYi{ycaC&ul)Y?|WWeKG63Kv)SUJkqpRl$+;3_Ob;e3 zEujukVaQ?gL{l|5-{T3G&1Pt=soRRSJd)-G^LarS#?!etHe68@5k@IG2s!Sa+3&a9 zT;E9ua3EAxIj>}G2&xHT=w%QQc=FkjIL)aK8}^4Cagq%U@17!wDC_ojnu;Hq48fT~ zbvbMrzvpsra{2W?tQ{|#p7H;WI&Y;Ed6w~uFTdpdhwl-GL^U;ehy5O79gF#jx~_4~ zW30h=Pn<3Z!iXfB5ylxhjEB%hO_HQo>&dc|Y&J*fl%_k;bsb-S^UZ~3d&lPK0|3cvPMW18p(JbS)@%j)`-G(ukZ_(f6h@%?Xww~0XgPH9$GHzZ+*Q)=QGM^VJ_ zP|+Jp5(jjBhv_8H6mS6W-p~NQ_0sS@3?IevjAr^6STchfj4VaJGLNzgCL+ zgh*?w_dI|64(~ilOAbZbwq!G*g9P4lC`*>h<#cbmuBR-eF|oGiem$e9E7D?t)(I9* ztP{Gv$6hS-IF5!QL-aVaS;qTszDC9Q1Z_r?`P!!k0l+Q3L za{L97b-p&;G!ZPzlBPM)G!@o5*6X`zG7`r*#+u2y{E`M>jG^yJ^k9*i=L`9N!0EID zuc+G!$^D4MRi&=+z9-Z%VHB{uz8~^RQsL_lh~(*XBGDQHL;glE6pw!5M~~>sh|Q$S`Xhe8q7#Os@lc-?KZN zxV^po1PL71k_rO6bM(DvsxIjrMDPVNkhy#)o2=U6v)Y*^KM! z`>C+K3^E-D36GEO&|Vyl7Rv=HOpt(BeNj+&g22=FgQlmcIBxgcy?G-!3A+uE4$wh_ z3dCYIEf$kD;}Vq&!;mnPK+@UrW}+`@&0sb%TAH5=m$4X+#Vli%5vSz1tc9nIjL_Vy2cjWW1%pY(^9+R$;29D^jA^1$d=H zacUZrl?K1->S@0}4+%z-979zdaGfKWC9LLao}b#>Ln6e}Y9Laovbz(t2S__M=B^W4@ zio;>g>9FPM<}G=i5d6+S>Stg6!oPp_J%8~R{|!L^ zSvH%TBE~<*3uCRsxlgd7ceGW_u|7-_l47qDX?U*5jrt2Z6jvH$jbZ@eJi* zOKU8?H{^Ln*R`x~Ukx0{=#zzgxv=7a+G4SsPBn+aM)-e54h4&Bl8Fv6ZkT$_7QkA-m@f5JY6Ng2UlR znnplG*EMwIj&!!7YK~ZIC2wKeEKHB{Jzdvxb^CgnJh;Jjcl5PZN;uq3c{Jw`ua}n> zajr=-@?td=2tq+PuOo4w>3Mnnz-oQValawWXXN>Ox<7Jldz6yY??`KuhR4TetnKj5 zF`q4H`j)osST5yIGtHK!Ht0~0oH#G2kDIBPZ<~g6zDB8NYBb0DpQh8aC(+yljY{&bzpsw%$w?SEib_WaX-`rpwu4b^dvib5hS`jF1FGRgId zc~Q_!Mb#w@NDzd!Z3R`fa?iB(gxQ)XQbTcarpk^5mP{HSDa(%Q>)U~zD|A-TQ%MGb z_a5aOhy9K?3g`(2N^ms%BUYdzH5iz9_~6(*JuttzLIO$8l+KZcAnOvubx79oY4=1i zze2|`S~DO?ZrIP{OniKN;cl67Y#aoTgehs7aB2={r!dyiHXY0ByWvdroYUMHP~UOl z8BtYxoU<&KYqVxqANmtY)p-3b?f2t2X8-b$I8G_s25TF#?20JVoOXMB2=n=RDvVD1 z7km(~Uf&KZ1sVPczn%-qmmTODJqmEv6NQ?#>v(+r$ldiT!X%_`9B4}vrBqdoQXygZ zsmo*(1?=}HG?r6c@#@uU^svtQ{#*oZUgX(Nvtp=0!Xa~JNZ!WMnRZ|VIK~VXIrHV2 z5qo7R#AT==c_Zt=paX>NJKCu?O~Y~jOn@TF<`j7@j&ecA$LEm3?c)asL#}V` zCwf)i_b45pl;^wezvZ*femc>`#$xl*U`C#6F}~1Y!s&FP+&>d#^I?c=iQ}9slf?7W z=|tO{2!SvX>vpFUvur_mI*`qmgSIwCP?8eS3=Z@x7|s*X;Ot&LpkvXyO|?*B^T57s);}BSAvue0*@7{fl)-iW?uMv^_ znL4$3c%b(!aWW?i6?s0Nnu8Icx@;1@{`%|Rp;KKpA>-?^0r`E%$t8VjY);0ybNrg| z*$u;xS9iC3czB$C_dofwKjrWK{_nS1zGtCv`UkXFPxQfL6@19OuOcIx7|!ccx9rB%c9Ii0Mr~ zQLBPb)7zdTD+t2)qA1eS0=1?`gqQ+}j*d#>P5%nqX;8)u<%bL8@`1f{ zUl_kzP2Lly8TfE| zfQ|7O+jD>OhQ2LPI$}OAfFCsLgm@LuwGE5)96Qj%p5BRlc6Hpc-MrwuA&gRjz+;Ux z44t>o?kJB3?mz$e;5-+gOevJQx+R^@iPPoqOplr#DXwO-f~ThsQm8yXv)gUhzdTc1 zUEz#~fvvF=S9dIKUQs@OfM`bB)FfGoQX<6fdpkLEj7`>lf5g-!r{k8UYuRs}dEPvu zaXddivVHo@a;z z*Hn~;9lOIL|L))ahX3-<{+y<)sGB2Uk`YA-Nt%*oIo5h?(@MrcqKTq}wy(LpTa)H7 zt`k?H-dcL&QMMI@lKFZ~nr5;Ns){sC+3zcYKpc}sTUc3#gD}{K)JK^Hk9!^ng-Uc- zg6>`(_{CyPZzN$GBf)Q*?UpRCJUqWpl_fgV#MzQ8pOIxV2m+x2c};cN?DHd}Ui`B5k#6oqkTr!`p*uFe+Jne^T7Y?)=NmLgq%P?HCj%-$lQ&Hayl*T2` z&mTFJN7}9?N%G;0G(?e_5IZ>!J=S^_i-M+Ya7L!mVrXD-uIKUbJ!v{4&kN!>WlWZc=Mc-u%$Lc_RIMG&8L*2c4!}jSty=lP*SO)hi2l?PydDuLo z^jXAyN!gkH(W2wiS~Hu^IqhCh-q2Pj!X)K**w9u7-n@B@F_yYI(YX%gg)y-|?6_Lr z5YW;M>t3AAg2_}vYfaO(DCGzA!jKm$v<}#9A6c%hCd6$_bN^v+J)Y6|ykNdqaNIt# zd443x64W44AJ6jfdja~sp)6~(j)>!g@|XVoyYKk#|Jh&AbR7GwMGKWb=Y_au@=o2F(G( z^rRiof+z&v4ltgi>qb#X7>ZB!Znq^0A_C=bfkF~q_2m2z1ko@p8z<<&02!Hyss(U7 z?hs+-8SavWAn+l;;UHojYg;^?I1+DdV-3N;E4^7<^YfqljBhsI1N=^E>Lolj-sImm zJr#cVndf>5@tsZ;TB||3@ zfz)l^p;W~Fs zIA5J9A4CGyeo@Q>i84}DdJko}!TXLdOetn7JmZvI#P)Sn z;=HFVchhvXJZ5)NO=Uisky2{eA}+j8VLK^M)~OX-wr-$`XW{m*@A~-@WBjS9lsw~Cg2xY!n7$${=0nqwFq#=iu-18h z6oI(#t=m>Y@J`z;qfj*nVuB_>W@C%3L=M3?S-+n;f*Oa>**<$rOhql(5 zwytsAGfG)b$0N(-Yl3(o^u)S{Fl4ow^YHF_?%#fab0eo+P>jG^dUw{^Z8ndDp=LQ> zLJ&>w#|bYD4fqR9U4Y}|>0ugd1xlvgIt&TZ0!%#=MUJlP1sOga$YyhjVmTeUlI)zI zloZI`d!9djpjcf`)G4ty4W_RjjZv~BWPhwCYM|tHoKVJKtU^VWEL+p}J$b$W%y17p zeOHS?gfT1@i)m_at>v_P0;RaQ|7_A-g<--N919G*deLocpB`8&=9nS(L@2KsW6gxl z_kB-QRTJ&D+`kYd8EL+tt52K`8+4cvXiw92gmEYtB~3$`W%OOeYsT$3$VksT6DFERN}{AQx?GQOXj!8kHWzi={flT|)u1MYsN_RX+5bJz?MNt$qO~c{v z!WWa1TH#o#iO^%n@G zh{NMJX0Z^V_xRk6;^wjL5ZzB8`-C+dtBz1BVlDTr8J<7u9AR3pTo%I+dNj+hbX_wH zw$Fw>NfHl8hNZ46`szra6Pm8!?dM-i?|rPJj@u{Z>ua<=TSF_Q*gU-_3PXBlm=(*x zguq~oqdx9w2mSD}D45MxLmrz93A7d(yHkVi3GfIq=GzTHx|)9fxb^_P`s%AsnzbLY zVaBz3*=UTfbB%g7sJLVjeesje`TCo0KdB#oh{C*m`xC}&ED559`m`4#swAcFo8fy# z3U!R2)RwO8IUQb@Ep8Z%LPiRu(qhG~LrvHAa;*Z;Uh`Lf^_Qrh{rs2y{rBJTFaE_p zr>aV_SmRVg2plUBsyOFJlbn1e$((T<)7LdF@FYo!_ll}IaXRg%0=Fnu6P@Yu0oiUI z@xXe0MG%Zm0hZ14GY$z)8bfYIBucnvdg!tw0onBZlyEDq9%k5pK5(WS_y>s&`y?g^5Fs;mg{s4yl<5`ti)kVQjc zr$$1hB(?dBn5+j7D^*LcU z2&7083sFPhVuDx#p}MAGUd-9=#X>I)BjQqyf%X`a%T0vB&~Pki?IPf$wHRWpeDmAC zBaAeE@QXho2m+qAPu#9=IaWKOAfhQ7isgd(xFee{$+86JLfY~>w$Cr*vz+~Q%iAyi zT-M9*+;v^c=6Q!!l|{PNu(s1f1R9-{O5pUKBJ<#pC;T1aVHJ6kf-S)S0@j zr!{N(j$%G1i9&)fr>YKg^@%Xl%oev;+mpqbs_oG_847@Y8cd9owej8w6{%zY@{!xS zx3osmv*ReHtWIb`oRWdYcDu*B8f-`yg>=^7y`{(!4y9;+ilUef_VIvo&heYy{LNs^ z9q_AP{iA759E-I;DVnyHiSm%EaHjlrSSvxg+m{EF3P|%AX_{YzV9MUrwr0>P%DQcu zlH-0$7-)KDSq^8#Y`MU8J#|-8?RPluFkW+ga|@{9nRGNwKXIhxcL}w#ZCmQ&fv&B& zy1x@dYcP}WSem*WIPwk4rG#LP`55Egz1zKzWf}Y37EM4t&q=ezl%_nw#M z2TAtsYi@5}6NYkz#Gyt9qG1%$mc{j!$ES~kL4dQKFcPx>9VeKkCQ0Y?U4;%4=0%Q< zQmiqcg8}VkT3=FJc}9I49-5}6st;UU-A~WSYMFRrKiI@0HYK;doAa=qr|Wk>HEv5qEG zer&Fq#?aSWI_rp&j4aEiW_(0m$AQebj?qH{+p*a^a)1AJr~$<8Rj5p!r>Bo-nal_a zqVJ~22)Q=>;97May!3rdlFXRT=ZMM6*|nsFmYz+F=LkXI-|udh-V3 zWjO7v!TNJH&$!=pT}OS~;uLrvf^7)m8In5gI373REE-1K-Q8gf6U8jcGO|qS*eDD! zwj)n+bfjr(p;q?%xCO5B~H|IF+5)CL2rJHstwiSmPa4wIk61#`LUj?+}~hK(3uP7wC;3 zu1(X?mPZVZ*=$MQ`^m=nOt+JD({&wx{nvku`tlcl=-+++EqLkLn3^rm@Ah3kzJ7fD z`1T*Th|mG)R#aEAH;@?hZkVTXATMLvZ)v?k>Rz*5bvj#WlFgm*-pS&5vZ| zuKVNWoRitJXJ$vKtIA z0Gd@K>^q64jJ~Iqi;btZg}XJJt+R`hHH!z(-P+pO!_LJMir6a(yNTt$o21;WEj;aA zoT;?!ovh*XtgWaxxTqw&Y^gZeIeDl!I0U&k1-ZGY)K#d|rM0y+t;gZusNfW&CA58V zy217!>oKp*hnV)e9@@KVFsH+DWvvbLgl?Gk58-EnR_0hpI5O^D46_fC`_Jz-%pP!v zKg2*5OL!<5xF|bprs1FZbO$HwggL_!g_Gcn8oA%uM-z^jF0KDD`7>NwJ6;WDT)f)= z<8x4|=0y6N+351wPZ>8R8a!-(`O*mJU-yI4*r&wa0@xvVl5kt|6&~b5Uz4OF;_BPm zLpe~s;YmKe_;-N5-yghHZ~jqQ{eaUxWB3FXIsat5ImI8)Y`Z7A^D6vkvhj2un34No zXaDpqZBl5q>)+JrDZcfC#rB^d$ekF@6L!JZho673|87kgs-Z|88@vhZf9VaP>}Ku8 z1zf$wb_digT4@5S3mBL;l*GEPuKje#t3uxAjTT-PfMr+LS6m({u%Mf7hYimZ?_Yyw zQ?2z!EDhUNoEBW#(hok#GQBN4&PqJm_j-E;KTI#4m@Ldmb-p!jUbP3BE&DA5#k0@7 zp`rHPCI+tB+E*B#@p(crxScmAE?1U(Mh7Es+k@Y5?#|W)U7b{+{?tgdCS6i!v$eUo ztDq4eWSc|m98qjKa}!UP8Y#N4&p)1{u&np>Mr;=~_#Cy&ikh7?wxTln!Y4E*hnveGUqJpX>o7t~|0Z1cfLe%wjwfX!h7)`}7b?{)hrPCWzFB;| zSAtW*Pvnf~I)8xP-VtNPX?Mu38x~IA@8T+}^^MHp{aY=#Ni9I2;k7k#CMKq}PT$;? zo34Q84*G{%2N$>7il%_O-_XmBsX*cc4(~~=MIIRq1lOA*oytmtpS8zwlXur%e+Q6j zjXN$y*5`UyO}jIC99ZVPq%=~L>3}GRxn=~rLo^0g8y3n&W~h?vgfib-=xWfRA`4${ z(#9R<_(AB0n+t}~g;Yr3DTK$`q{8~cOo)D#EM$D;W9$0GibLJi0461>@)~Bqb6hOr z{3Yy)gzJ24nSItIzX1J9WpzJlJZ5Q_g=86}l2QyaE9*b7t%F1MRPgK7^I-5jZf0(- zh1U%spR+NlIS?39Qc|KS7CScOWCk{sG2sivTUuU*ohUA@rck_p!J;?6{nbqPuKmE1 z?Fc6)C+tE(1W!BLoEY|XzdS1ZZR8zf_q&-fK@G}u+t-sVEj+4u!X&E{aM@?+Zi)U< zXpNV#{aq+VDK&0sH#VSOP*?sTz{KtZ)cta2OvIfDFkHIn6}Kby(goUof|_kNj7*LN z_{OtDU*{AuFhu{T((6?+S@eM#=w3CE1UrO3T2P=|Klvc+fa^PkFAr_~{-{XREWqs#kR{zulE!wTtPCHx=kSY5 z?aPm|E_{BSokiy6o?ZtS{6wA`k&45RAfC_8q&WfvFK)$@ee6QA(aFRmxj{L@|)HdPH-6Kl@EsiN17 z)RMCsjWd@w(?u^sI4_gAgIiSVd6e>kb+0?xN%C);Wt!pj>o*$Z*%q~ zuCJ&!4S~K;YgQwNbB#rbZBogt#h}y0`+Hk4oY>KxtGK5&=;iE)n93Zt0`;-%A6~ld z_uq)RGHvIw=qWux+cqv%Ydh~lc(y(PrpW3+AePbv=TRV8fdK0xMG!04#os?WXj3aq z2B|#_59!lA#3Ba4NT1cXJwfy;4=E}43I<=U{N_$Ctl-M6e;)5UUi)vj7bbl{iLEKt zH}dbcij3oD+ct5>io?Ti$5xW?gg5vYf)Z?Pp9`L1O(%H(T7SW1436B)*+}=2#$1+Y z-{1@r$*9is)Cq~G`TJx;=*ZZ^ST6dt2vLCJ{O>K*Mc$r0QmmH;7?G35GJ<;LG?t2O zk0JEtRog_8^8QNe?U4!y+1c5@Cehm2Vi4j-UqVoEJA&WfCnhHJ9qxDN#WEoU%oFox zB_+exIQ>S9n3vCEK9V{*_f$_UXA)OeSKMB|Cns^Jk%;&9K3D$ebUruj(MzS((UFzn z-Ja+-PXOWed%FpREO(5U{5zuV4ovYc{58-v((dGqoY&g>XJ_B-Pv#E$%Ngiu z6l$X)S36qg2)2x`toRLxJX!Dnu~?D1J$&fh8+`A0OK?%}hRvNM&avT@@FkitH2kIJ zd#$CUtL;db)zA>It|{>GNR0Q%Rp#oOng;jwbP0wCwLaG55#S%-94iAE& z4FyrD2#T)-bdh})*GS{`Qn84!Lpud;0;D2iUI)T3yDyINOJBzHLoO0cxJ@)Q|1`^8 zDD*{$n=JaL_^j$L;d&lE|Pf{js0Fr6SW z`D~7V`d^r%3+FZt^HH5LPDMXhnJWjoJa)`MeIDdQRYJ-b5W^|}59rX|D|og*VKJfR zzMYMI)PQqk*=;1avdv|k6zBMhl5EH9)8#1!d$Cml24#F~xDY22yf10x((#Z_XPdq8 zEwr$c0r**o!6DfV_)2$MBvJ+uXGZ}dtO|9F;pJrK6A-T`K1DB?W_yNxLL%dl;T8<| z$q>^GZ?LqoHUyBez)tS8v3E<{GIUJQfn#-=re>;kZ0(%cX^3{15d!fbQQ8?}>pQP^ z${3`%Nl}lNTZ{e}EUcgL;UI+k>yC%$Z>S@}TkA@(Tc#nwQ_^v=cUDw&?1miR5%8(< z5Z9a)36@1B7Lpa-&OL~0=u6^lm|>g0|DGVRA}YqGrgOWrwn7*_N31|`XM{QD{-{xV z=v8`GR}(0%qmv`tIzGv7j)TAbwGpoeYoWVOr_eO(_zRyOq?C_|9^V@YpK0#&`(%KF z!!LArl2UbiKP&y_Z;)u!tj}a1G5sqD`$q@X_`U(BEqp#_9kvV@so!BJfhJr~ zPXv$Kubw7y)EVXU0~)aJloxp05$R;i9mG8NmuhTevFFyy#@5z8D?BqZ6Q{O;MY%!` zjrzO)%Tx0S^z2O2aN>RMy$mt&-o?h%UbahPdwaqP9p{G9Wv%#%fZU&BuZ?`Lf8oSD zqGlILe0+GowJeF67GJXnG9Br82X0;=UI0n%JMPJuc<7&?a`=*P(3a+fw2n^dxfZYF zu#~0KZ6t#z5$>1xLWZLKCdJTvQra&$2s8Xg8~`kmcyBIIY{~9rG;^8vkpXEkQCD$H zdbO^v&kx1NQU~v78~I4+lEVu}+e_BHmy>U6#|jxsHa9r@?-@$g)M*OR;OpmO)mVEv zYv*W2C$!78E27#gI4d%hA&xFCu)2~PTJ>AEy0%tRhbSgSiJ67P8%cxEO9-oiC4(AA zI@|h`vt=Bwx!>5dS+wgjL1gq#>G$&Ihg{a_o!`_~V3kuwque5cwJux8t?3K={WIv7 zBw6x$%wzTK4{TY09Hz9uSoN1G7zcb!e|t#3AA7qT`6S+iw134XIh~obX{6hXDM~_n zgluOv_pSZqi5t#2WY)fJntL=4<(CuOIm6(#M!2f(XdDbjUEAN6pDecFJ{d2Mb3=E0 zHMffcRmf;5Z&q;8M2>_g7tU^PMta|L;7sq=Fn##;{)qXtT>UDoD59VAhLnIOY1e*Y z8m?UCihr4&j^drrJUP@`4GWy6Gb840;Dbj@?Eer8Sqnag+b0hRiVqu>d3>QBiMy$% z^A`dyAs_?(-=_~mf17N{YkyjT2NO8mGOFY}GWV_Ga?J^oKb02*~8FmH%=%OE44 z;DZ&yAiETmx;}?hwC1X!quO`$Oth7i4aLx*lN09Ji?y!a$%0+rH;M2mbCvHLuFv-x zeX=gd00c-h^$ZGIp#XNaQTEAM9NNa%Cg|^Ufwg!yNCqFQMU8jfNQ->vaSs16Bc&q# zxPG@wa)iG4Z;Bfu2AZg!EwlB^gCsed?>i1|g>NEQ;fSP*^(`&KmwyNSt!%_zF2JUz zUiS+|E)88>N^{)IEH#?`Q)|Y~*C3cEzEcO3-!p{G*E*I8XfEiW9$aY-wIK6>*=& zv3!$~W{n{~D$i^3ud&ozJ3lI&1sBJc-CP-if@4ox+w?|ehKcCHdIfrggThhU_0a&@ zqhC7=Azj|k#_sMEEb?5Bn;+>V(t~#VDbWShdejeR8{mn=Pja)JH6NWPJLw|keCr&F zB9GUlmy^8d({gq1^Eo~bSPAF3-kxLv5bPUdGH)+$gX)~fW52@C&1uX>-YFlV7)?O- z^Ez(qjJG66+N1X-Cn2>FB7!m0=ocK5J@#+fG ztvAJ@%@MaW1XGSSgl4;of6!WM6dRKXjRu0EiE5L?wtVHLO~ z<`RXV5qu<^b+I3`44#cAPrHP^zPvS!@bjacpLwgejGuBm*R$u|wrp~hZZ>T~6Z+q* zIDM_iR-r3U0n}iMq(v3N2*2CTyo?tELu5wOw&2GeFkBFuLs!1VefhCb@Hva<&GP#@ z;l&lnrCS@pJie_g(nWq!zWxjfFA>O-c(2$}8|CRfOR?*syYF~h?$CxlPk-QAHsVb=WyOrS;%Da57FAb8ivC>>rc{8!ZQZv~5934J&ayo#r8^ zM*4B&0^j{&SFK{H)t91AYg9=0GxE(6O#S58D-Va7n_6jnrq*I-E!m%Tqcy12(7^t%Yrn z*k+p-$l5Ufc=k1KD49)IK_|_)-8F8{m_Vv)<#cURsByFB+=BC>{AGWz_7Hk>??8xZ zRTpG{Vg73aE5ZYz-XBashcx~diy8(x;7KL6K*n8m^ou!Ocj^3e>;7CJ?+&+Zlnwu{ z-rfxhmvX_^Blh6#=gTr6K&yXYff&Y&A^JA^dzLhj0Orky$yH`%X1ZC${k`+QQu;=8 zoDX_IyD*~gpJF13_PpNv1Q%Xx%l`N-<8!?kBgcVB4wPd%V_)!>^Eos;6()#posrgNC4}` z_|n}gVLcuID?HSEF2gRv=cs7x#W5>uFj0KWfdks~gCS5FtS%O|UwO`E&N-Ny{cC0CzCl`r;v*bUAJR3B60JW#hQ^obgR9gzQeUI)sK|Xxsib%p;Vr6XBINGAx zuI~Qsk(3Va*>Xx2F99U}Id?=UJ*Dd&SBnZ@MSnPffybszwxPz^M^>HpK z<|fZ=^`Ga+>C7isgZq=2D-EKOk)LlH6qNR@b2SWG7vdUZA>pp{FEA!1YX{Kp_Y8u& zv0_$^AFF;x=YFy9kMx2xS_1p!l^yXK4{cl+m6~P;s@ms)&raAr3da<8`Nc7ms*07A zkoU(Mp73PUd&$*eDZ-&FyJak5V89xoe0G*!lg)l;L6Ul9xl6-Xezh+%U~mtDBBR01 zC&VPH6p-ff89Na;b$R*DY2DuNf#ES|eR^?WA*??({`16-k`ft?lj?$^2vA}N=#8u2 z%8Q7EB$tfVJC0niSuR}4;kT{&t6ltd&|X#fWqv_psEg!yHWr#?06WudL- z_t6oCkkBetgcQyP!B8Jur5w!9tz9CKr?xtOHiXeWY;QiarB!C_$^~6BPM@Rp26F^S z!Bw99Zd@D^xDn-mei1$nP?$%g*=xNq?MF$2$*EYyLsai+NzWE3LX=#my2uqJgXDo^d8?R%EyAK z6QTIcx^$qSTCmvE>J;9BD=Bt7>c?-PH#gbUH8om`2n_kYd-kj*esbc52C&j;pO&8f zc_R9~S%5@ziZpI?OQJeDJf`*w#r6yuRHRI`=0DNz{zJ?j3Z4S`So-u2l$Qe?d2l?6 zNB?%b*}>NL(uxlMI|_Q$oS}@q^qyUV?eK6| zC~}U2W$*Clv{%xF;YKBQ2Ax{**36kGHO!8AWd6ml4(JGIH)(iR7PK+JDFFu(eo5SYCD+++aq<( z-}|kgkx!2=5*U9Sfm{hK8~js;b7kB9oTlj_y+O0HXoVl9BEUX$Y>stJ<3uTw!%syOYTp9<%5)>FAHf1olsXs_uWd zRlew>Q8+kSZV#y+HqsPN-Rl0Bg?H*o>~vIpkJke4{zUSaizVo>Rut66fT?IyjDhGR zg+k?H5uU;`{TepP_?Ypl>`C2UxjAsdpRNdB1%$~w&PmtcCE<+r9k<7yh(P$$d(9xF zRWCBhEw;De-UP_(ldim?;|=tsU(e@!3w}!U>Eu)Y<@JpQT*IDG^GN&4gGBiF^` z(M|aiuHU}Ii(QuB@U-Njev(y7juH<`dY0n?$2bxR#5=h8$mNNY@G0Q7T*b4qPH!VH zSa(5mdLMg}4GHI=3H;F^~F#LFhb zbQiipyzjqbv5C{uJ~OkdOLGHKeAK6+_Y!vbjURqtGSHLmCKiuoCjzoW`CD7PFM6n@ z@pR-+z7PguOr%JVCj9HCT`RV-S%TIh%<|4$)6o@X^Xq2C6mIW^&|yMCZ!(@Ke!#h6 zPhi;H+frD6?zCv+SU{Nn$eM*z*ZA&S6ha`UCa0Q|-mtQAKe_t8%lP1Xri=zW*wIl@7YA-{za6!jZG6t$)Eqqe;!*D z@=dPmzQhQ=seAnci2lRQMgH!`NtKhPEjP^GaTE?1y)2M2$9lU7zR|ls%ztpX|4aF5 z5%)m|rQDP@tkM?>z>6wXHb2j}C@WE5+?Q?TdDeGSEH4}4O^diZ_$ z6QFlD=I0%g|CZVN3sy}$21O&(atgS20Bm?^1L0H(e_}%s>>0Gk>dRZ~V+-7PN?6b# zcvzuy`|^=+XjcWgc4W+QhwzTd)7SP}oWV)xExb}ir@Gc1i?q#)=XUh!w$O-Bn*+-~ zRt%%zI{QJC=dw#^K=+%UR{SK(DNPPs8ridHJG`5eXx?z;_=`RUw{t#~-%L6^KL@JB zcC}Kq@y&1E|K40jbR+^H*n4E-GzcDK-MStK-2qc>xn5Ud)K~xIDiHg>i(|2+ew`5G3fy)=yx_yiDGq^k*fng ziNf(1!y$@Feh0`olVK6>oflgOx5|oDD63xkGd-UOGdfhft}-K&L^@Q~oTIMNZJ(SK>v#gc(JSNz2;o{=@m9@}Rt>9TtHvk!SrDKrS&4=k3>ki4BaVu|Z z@WWnqL*q6pTkrGbd}s0NZ>W z;hKxEU~5=Sbnv!#Z`L8*a#bFP|3gKvBJcwtuu zHV4d+cwU=&Y4bH}Kj{Bo7NGmv+qT&onP|Hkm(G{jcO7AWa%iP=(q_uP+I>fI+Agl` z2tFpRDpR#sv4?MX5oFs?kpH5SIu=QEIbP;P#lkpCP6~XdP8mNk-%_NPS`pgfr*6fJ zoqR!KsW+Uuye0pqLs@7_bEQCb7G_Cgg6PdH0gjxeoM#!zbXX`M^6vVeyQq2m=klF) zmD97kCuKT<|BQ&4PZ0thd&=CQP9Fz|Tq*}8Yx{O5^z;o3EDopLli2?7J;}fw{(&4= zo?mBx9Q>3fv|mI^(!!G6J*}%(tcu#C#xYe9(SRz|1G{}G0@BoeI^0+jhULcxL{T3! zWJEq3Gc7bKWEXJ=Wa_{v_C1k0p!#FsBC~%1S55z(}w3ni4oue8itxdB_ zRa;5}&q1<4*tuMQPv3YNl}GzjWt}k57+u27<$x^jJUYK;@5pQH2m-SKKWfM?cbG_t z$Cfp3&G%&?1U$j(ALz1F9cSs81*TS3`aB+%i8a1!)m_W9Ht+*uDWi(C@-T1Y zthFaWLI)GZ;7>7-X{sF~;Dy9`O-Z>W{7d+>Ap!b?FAajWV*k5-Mp>y|1Wj|QBA<;1CJJhsG)&d*GiZyRg{IQvalZ3@8`9iP1|}6ir>xcQ$qsx8Iu?T7tkyYx}_8i zn7XpAtQsX!uL3$Bl1|l)OjhX-$}?~+R{m!H6UkHla<VM$sKQfJsN3R9{kO_;v~UI^-ha&KWm@8u)MC~K zD!GUs@tHZjd5>5w&i$Y;@y$Y*BI^B;;^FS1sef*KuyDRml2_uHli~uh=tb1~2TGX9 z5Ogy2h9>rCD~5`)5vbR#6nw$+cRZy&xQt9xlvx!o2 zYw!0exmZwjm>Xi46Ml#VRJ6vrttK_0mtZ7*|BggyyOAPS`HLSo$geHDF;6TwuQP2; z^CnH+zskxguy{hg{OR_LxT&iv1SOjKc%^8ClJXJg|fz3syZXQG}NBN{-eo<_U6 zx(fJ&3gJ-+frr5>Md2)36uh(1CbD{}4y&62l}}U`fz&T^9$#TRY;g3-Vnrb5camoS zhkudGstV(+sJgDTX}Q9I2}1j6rOn&}@qdJ^sj2CCUhO62+pnN}*xq?Mhc|xltBW)7 zXBLZ=Tqv$#Y}|Qx&51A4^Y6FSr5_ryuuiOGICh;76y7eQFbYSI{ z)!SF&JDxdIukK@$Pa>g_GSTI zxv_I#Xy@R-4q&H<%}|ES_?KUdU~Be@4(aQHdrwU=D&u4$`&vsGDpmc{;oJW0Jwv)xJV>WexdUrk7oi(efIsD%};&4UpsYseQ_ujoLV0}Jbtn>Kdr6Z4R z#$_-6P0#xFa(CjP8U?CxZx0$OM8(avQF_KZ{YO2ip=*t_D4Lc@SmeilfbNicl9<4& zZlsMD@JoRUO@Y>wD0#Fy;{#s8&aNu$`-^G=0wdC8H{g(GW@aw@zlTrXSdh+toV+K? z&Sqv|6=%;835J#)f(O!V@3uk4r~Vsws?!>_B44Lbp1CUIgXh$fJ}URNZ+Y-s4E|vPIe* zcD;9iqKJK-v8CCH%MAj{Kjb_|?lZ`SepIo9cwpYF2j6dwL75t);hmo3c73IyqBxE~hgfAUv2Ti29NP05aeWXjeL;pv4WW z=x@pHb#A5ywlOO~im*hH%WdlJxZ&yYlIVzR@Ml@#->#;Da~v5aUKVS?Q_D^J30!8< zLGx&rSwi+DeezGgl0GJJOm^W+B4QIDvuiSs8ooZ7y&=E0vAxvIbUE<*o#-bGH!=f% z_?Q;KmI)T5;!0Co>^8G++wizPz$X+YKuEsGcy$Gclvm!kzk$8S=BOfKq8k$3aq;Ws z%-Q>%@a{AY?y3NBhJ}Bi0i!d#ouW`VLSJru4Sy&jtev8WFN`!*TV03; z>;Hxp=@1hIVr|dxdNj!`Z7i2^1EMR_nW-EwqX8eNIwve*H2-OSa@AetkX>Q}T6Q;b z+|#M25M}u!XA-|hmOv>5+m&>xq+6?0f1t6Ekucvd@BGureQSbqL{iL7XN`;UA}V7~ zP=x)@kL=p79S?wm($JmrE%xaS+GeS>n?MeX1<+i$0``Sl89pnAcvCo7H^vn9$=y{! z^nZj797=o{L#Q>PoqspN@TTa@<)CF3&KbCUk+It5;sHz5z?9Nk2C{Ev&%HjukN0Sq&yU1D?eLi5mR4YdS z3|m`=uc+z!Ap%htS_@9qo16T_!j%x>eGlrCi!Vng>y0$E286{t8097PBje+Wwodk| z|8e6}qc+P6T>Fr54IIP$ENATqkE^VW5rt-69(?G4(2S?X?q)zN(;#FJd++_8y>apf0nd>ast3JK zGlqjzbv=p(X%!&*q+NLg^Q4eskJJTD$i?~Jj0jv8x9bCPAt59Gg$-0n<3ax^n6dcl z*Do*65g5Tnr*_?jkb0l3SqBBhb#y$W;R73VKcROD`WWVa=E}tZXuFtVg(!O1@kEW9 zVKg2kW|vl_cB*2C@G{i9UxxjTw8;pxxA89vih6CFW*aS^RoMJcA6YGtCg zOF#gSKlt^TFmlsuF=%PiD)#;7GI5*&O#9!D?2wFiH3KDo{#=DjR0>#q$K8^^)8AO|1ch$|gksoY>i3~h1nm+AH1;QWV&hfpyO zesA7eKd`8Z78hx(=J!uL&n-x79D@HQbGyie{FGgr-mCdiZL3{aWyDDs=+f3+%&UBe zxkKWv6#szYcm2IuNF+&Wk~;Aq{3NR4IJ|Ovw@*g2aRU|vS#5)|i-}DE<8z6WojCWL zkHQ-n)!DLzyA-dMdTtX+k&eb9J2I`=!jG z;kUMz6vpTBu0DN-b97$cQQ*{9Gc})(&{tln1aLJPY#B$(QPyNIKnP#~p4X#KsPuUjk9u!ww zP<>bWOhDQfa)k{@CbY8*ot{QrKiGXmvI)S_PymT<4Zk<-)1N3&O202Ld!&6m$9+o( zepGw2mxC*1`%q-|x@&g!{Gx9~xv;pHOm<22RUsug9z`c}MQWn_R}3#@7C%dhVTZES zNBV}=jg+&omX^El;RDNAnPLUVshb@c#v`A#%59uhP~DG_MuxuD<)M3W?uXwRIxiX3 z{Gdy^vC4<2%-n>FxjEM>*~IO@-LAQa)G$1syDq&#Hwy;GFqT_gj^tYhp3;xHrsL70 zcPLAelC$HSeLv->T(35qB6okveBG|4z}G*kkA#V+CK&&-A%u3DO1F&Pp!JhZHbi!fVh?NQ@_= zNsK!#Vwb7qt2!mO#^m{K-z@t4`?pOl6zJ$8-3D2n}#?Tt4vCZSL)i5deUITb81RGQL&d4*BNuHXpy&Uqa`y0%vWt9VsSy z`<$*YrmRwhvCBtqFzI$7Ws!o%r$=t$#mI0OjWi!@{}wtr`A4e+?%3_WgTHaiwFjs| z#YvbPbacX6!YzUsP^vT#2c*rXe^YBCieI8d?WTx;$7Mrk>g(&_kXVgM(&}pO!&B4lrQ;zUVbZ~a&9goQ4W!*Xgs)@!nw(z-Z z{DXy||B^*~zRV=?5~0qAn$s9}ZtSf+VCDz~=fUz!muJUNc+DBEWFx}|rsWZP;rXiN zyec_gj?z1v1mVbAFIYA|5E|wjRxm;t+>LBJOf}{+(DtoJw8(^bv3VtjsklW7cXe2B zUZ~1c;Nl$@E!(3DcAGXyy`6j4!B9ATegGr94_HpvvTPPWikSbrLtpMdtpNLRdC>&E zYz4lGU%kh4N1qA^x$(sFqQ2oFyzS%%MD@P3z0rS{8UvaO1U?cf`T)%XUx=CLRe#D! zz`csT)^kD{WVls}jGrrmxL~s?q>W^a>nkA#s`BS>T7I0)GCo5h1 zLACy1;KUMf_ znV~Fu?e{Wo(=SU4&MT;;#}BqLXjpLnHx#vyr#g=~(}3p|tWnN-6n@;csNw&cmSg!o}tGp6{gne3wQ8_ziO^v>SJ?H4@IUX zejHoCU(9E4@7*0c#P#10`s-T$lSf7G95e7~vr%1J0Ap!t19i^8cv|&&Dp(8er>ufT z#?abctooqEN;EU#?^gF*Q=~^1VDY?z!Obk)dxQq*%?*ETl$C6gC-8XlmyOgRnUVVN z(bvTmny!+h(94fMBK(9AnCS1K-7Mn8BqzHDc0w*VbTY$#16gHECJVkUG zw{C=pFN8^|?K$Pqk=@gogAZv&HzFfUKDi2p=sBNzQM zuW9tQRx#&h+DX*DLW``T)CxO4bL2GBM3D-i9G;(bzjg+D$NF*h69;biZ3_wC%xi_h z$S79VCyk>5Y6RMKA8k4W+o?PyMPrjYuxY$EtI5A8dQn-Ec;BNjdTTH-Eibo1Yqkvt zH?J<}^cYU~^?I9J%r_cVNiQL*=>)eg^GZ`5kM|9p_w$c6sODHqWqPpj0btb#%+fdR zc!q~H0Vm%a?I>GG_@vpaDPsm82|UOi9n&%>Mi-OquQ$dYKH#o)$|)c$8z0-r6CQRw zt`*k%&^OM{OSa2DjP#)xHDf!N#7je)ThbmnmJb9t4jm0+67iYwtLbYe>M!VUw}n)k}Lg^XoZ3xQrzq z0)WkAjNKeBK5T_J?`A!}6boe(d|90HFP|)l^2l zzQFLV_vYTfHl@@g8W+nasU2cVfxkN?i9BX22=Cvp->q0bdGPPwAL;c%#vh3CeD=_J zy?JHwQP7heC(_fn{v5yDb?J1J)`dj2;YpT_FWmX{iXv`}r%u2m6mnR>nk_^heeln8 zP3fMb8ete(LlH)*@-yD0x-+pFAjpwoL|4pEuREC;@=X96;h3JJ5_J`!TPq)=X{}?T z19@n{rhhUWfab8_mm#UrUC(p6v|3$PtlpQd-U+qmp?IL=@s_PJcOK_wdLX?!o~87+ z%#XZpkj?urpR;f!@pk4Kex*V_i@-n=;X9qwh0EJp9GERIW{ycs5JW~s1`C4HZkvga z2?+@iE+~e^r;|&Gyl%l*6>P?V`2F6tPYgA^J{&J!NwYUudhs?HU9yi)0RSzepFI)r zs9!!J!f^0x()r}dRJmfimc~D&QT`x%M@7|lAyHc2$Y}ria4vVeHM*wxx&4U6fo1dM z>`D0b9(kNod3R)ofp*>2bKAw=UeqxbHnx*9=yL+N78y~^DZSpc_O5z% zWAnA2=4bP-&T-_VE$Oru)Odycg(pgpX%$f~_J(7B{h-%dN105MpZQ~TxN_o4%s&2k zzq4#@pspsXIrb6l>5C-N@{BXO?4`Q)VKF);JY|2PoMkId^+59@nkMbuV(`nNfcF_W z1n}A~c8?lx5o5M9nJe^!A&2%IR+Qt|0uPm7r}jthb^7Z&m?3HEcjsx@nF5}W%-$Cf zok9(8&5iB%M_*5{x|=r8=RBqM`w-wKYcVf_UO41lt|Hpb6>-zNT>kP%60AfmBaVex zu4P7_tYGO$#NJ+NpW2dA`Qg;Z*a6GBF5X*|BXLrocGEDuePXoKMdfQ!VcPFCJi=$x zY!GIRCn6285s5V#2ixb7sBsJBtRfw%RF^_dTzHTRSdT9?=_B2;jkp$GIYBWO;70?% zSa_bfQIMKzO>HW z?|np=WnO;fPw=$+&*tfT13L{TcPpV~t=%Jp@@0lH*6H1+V^@ogU?`o^#E4K|yFHzo zNL7GhU;i!>)a~M8#Qq39-mF`Go0QtH@D&D7H8m_SoAx_0?5#HIJSttRKmYY2zp-6- zgzxnr8!q5^Zc1}8FZTlHno)LD=>F7%!db!OsU(a-MMEEAyZ;vDJzEaY-!|vE z5i%YDa4%ZzjCZfuHATM!mpWVd=U*D43nWgd&2r{#pbE2o$NP;bv1vU7EMzVXH{^-^ z6{KNzxX`*^8Yfr~C19CYNdpVfqYYRwZrLB7HZqr2Qe0garyg<%&93A{%Po2Z?~W6- zw$cQYcZ2EXiGr8tn3 zY&+}^V$m!&Rfuf^>uXfV7pobh!Saf*<|+L;79iv`!i6eI6l-S&HTH`cj7Z0aqn*ng z=mbG>N63paB7O#*7f}1R51UxWo{q|_D+8`k+ayAA;)SNXRI6Db|(r zblpdr{h}rs3v1 zJesr=jiBjv36Z;n96ui@ET=Ju9o~x_-6+EN*i4{l_f}Kmh|B5FkpHkk7M~PQH+ieKGP9%1Y|DT&yIc3tN)?!9WHr&TRhQn}$7-t6nzoI9EsLfx(L-0+y${*{jrx7~ zKKpuTd8Ye?y}h#(RF=f}DLHJ4UZ&I|5m^%y2N3|_vZ$#m`^0!bbll21A5FzwHNGaF zm({gi01M%>i82_LDs>8Z_gk%Q8yPTbjh|}U&6GZ+GRbCsP8!WPIKwfZ;GyF|iY>Vj z=VN9b;2RnyGnhmhEwPM6&e9cF{C4$yAWfX$iD31wN*V#;)@-r;Xc}E}*z`0%8g<&m zZ8#n&8uIsFJ~15dNs;?=9`li!3o9A=v)RUZ1l$6hASd`FZzU4()Xz6T5~|HCp{}19 zqYDcc39Nlxt|a6NDi9VN8XKZ=Gc8VUoi+kau~~)P;63CaEFt540|V6E9&24qkjup6 zsFu0*Q>OII-3!e3<;UTe`%@!`IEV#f9EY70^T+(;*ML}h*bG@!@HJiV>xkIMME(=E zid`Z}|7;b;xcm3mT$YWXZ4(&T4dVzmX`4_M2)WC92u+<5%+ub=!k`?597rY z_%-AVL_ZMhLng>flB8aY{yAfNiZou1%!L4}PuEJh)5d19MsP(SCH{H>L~*4mCU^0- zer5&+$S|M(7)B3Zq2(OQdnq&+%>lKvcvZJl*F^n?zoZfQkR>R#$^=dHX{a{)~?L7x7YbobR^UL^;9rk}1jvk{MKDSL3 zTboFZv?4)akESdBI_p*u8QApBcRa?=!@^O0trh61BJ}4+RW9d;YQ55P+!x9}KZX?< z88x=^Z(dTwoc#Xv%O;6oy!TG^c4TSczviVmf;NIqE*%po?a^->wS?YvB>N+aG>!CJq+DSg2`%C3Vv;d{WMYd@qa5 z$83I3pNYw80T5t>ja<+ZX@3W<&$N!dq(bUvg@aAF6vcnxqRV%d5n++}RmJZ^`6;y) zRj^!=z?u^c6E73?thBUL#Zz96I^=)aGnBvdAJ1f$KMI8Q)vUG&1U>r*3YdEKOrS^` zl=N04ZW|A>=R#>->c$nZcGHC0ut)!or*rVGyAQU08YhjD#8lJZQHhOHMY~( zc9S%=aevSIu6zH2la+POd}sFT{h9Zy{hp5%<+>FqPb_1^46H}0*QRJOl2k;zl0{C} z<8EkM$aaH zJHNM$_vx#51Q3SqdlVO)yOqy%y#pZAApxqd`4ocmJVe3Quhw(>!}qkzH?q$6p{na< z1P3U23lc~VZ?(DM^X7PshuOIM;9NJ^2uh}<+ZhbUxV6Zw4%fT(%BA5yei`$0m$A5c z&gM^O@J7Kc{neQLX-Fr3V!**&f=;a4N63>8#LB&NQ~SObxvA zfV8sX+nc-Q8q6K9rRDy8sOy$Jz-1ZqWkqmQVK;)4`Xi<7d4V$1^UUgEWN1S4t-h+N zYI1sNFZ=j$i%$DMT^sXSpCpg0=!`UMXqE_MdD*u3>T_3$LBGa?2fK-7d+(=R4^&?n z-wP|jW?LQ{INr$u&1k#BT`m%NPswrV1aH-5HkVWBLCd|QX@*d2cJ@3JbsQji>D$=Q z4eOQ7v@pz-xA{Ebv#e`)hamf`|Qs;VIt~Gm%_FsNVU!iAQ@Aey_be&qAId>1LLqPsNveh7-RkL=}cmm_hZ&gUK7+Cy?2Tdh4?se2(W zu=VuhclqSSJ0a{iYXCNK`ZeXN# z_yk|icL^71Tvb+7qnovv4?rRUFn!~~tI`SM0wL62!X@Y!7%xkHk2B8$$A0K=`z;n1 zOB$k$(=W$g`+7q6=TyELdcEV5lW-%Lz(Td>stpSDm)gi+G=q+B*hS36w417$8kgt2 zxuxC$E70Guh;U`|c<^8=8kXDG+DbeZLPaL-F(&c}PFp8VganX?#TwskSi#(7&+f3w zz(w6Iwg}(J4E0qSA!rB;F0yH>j!ISuQq%v*O{5N;@zf>-YOcF*`=LXz z@1c^H<*`vw%o7JUt(`Z-t=%=y?!9HAs8ZHH@SR}#|K@^}%AryJbtK@muw}$v%5Qld&#Q;TOc^V3Aur+> z0~FQ1&h);x0dt-s%3^og=3R`PcTnEfOP-mJPfqVc>nD2eb5t*;5qPJO39GcU0{a#d z@{>VlPcOFrgmKc5@FgmV%569=dwQ~HO};uRRX1YAZLAYK`t#)j4w{IF0<05BZihHs zh=9Ot=Ay`j@ZjVo2=Au&cB&j#>hquMU8-<(L^^;ogCr8I+QmXiD>MUcW7=jmG z3QSn5s=M_Rk`xy3#w~gbH&S5!GBl$2h8QWI25) zDNMchfwA*W0c^gwp+R8>G`rh96-(C# zTGt^jd9&{Xx1+A(Y9{oobeX>>IZmTNazyiv7g$9mge~w9U-6TOM3NoG0(Z&)W}ePrVJy@w_$g zzzDNm7$@C^P8u^_4?Ck5UkPveMC)@Pz?{a=J32B#J{m{x_j0R~$GyIPGWu5n9F%Zs zYMT3b!=2DR`E7oFE#CI`)Xc1`I7}hx@0!lOmOKG+KBr2^UucbfBc$F;mJgGp|BegF z-CFz_O^5Ss>#qE6Cio9_K0dD!9MhstjFgXvv1s0`cBgB!Ca36T2@trS&!#wbHr=cr zGBdLJm-w(q7o@ul2LT;B`RY`A@br{=nhQvn0rgG-IslbIw!Zx)et38ovp_KMxH9oh zrwiRDlbJ6%AqX}GIcN3kLE);fMQCxQ#oa@+C?R<^t|10HhxOuN=^+I0t*6!^K9}zGe{@#oa?4a;-C}E!Q81?6d;LkcN#JbN?Pb7; zz<^}wj8dtHifY?Qv-U+T&WvFpZzQu^k|{>{4gr}~IRJiKqD_SCTn1A>0+%^B7fYjJ z7s2GnT|vF<+ErzNg<*3*ILc!dU&mBi`5dK=!ynwm8v(J=!l)9j{4X%@4H{*i#rrzh zgu%CV*?rAYizgcaOUzJXa3ED;?QCf~T?ksc%RNCVjotwW+3`I##JO6i z9*V=eMqZ@9WDxd};2}YuSLG7$x%4TSQELI}PQ%c{S8sLqc$0~kE+dVA-oeqllgC83cD~O1ViEU)832GuDS|YtjAE#J6)t$h{xu*#; zdMsCbha-?42ZERO`vLrKInSX1y2|OR6TSrk7dy|Zf3BU?vXXtXs8&!W3Kh zNk&O6USK8+m>sd7uB}4&3mv#b*`kR>q|S#R>dglH5BTHSXyBd_9}^m`Lw&zX`M=&^cJXg|~cwr07AHMH}1S!9>8}nU7B~gw?96C4RW@VwT%rE z`*P`)tr{*wP1Sumt@u>$Xt)|)6!kjSzI`x=rWJ!%x9-Usv0CR=Y^)V%n-Du1iHi}R zNu)A;^J_K*#b|V#T=Ztv#M;@eq!zpD^nAxhMyAG06AbU7b8>h)y6KlHtH1v0ec8QdeI58gmEYq? z@L`L`C=?ff#<*oZnQDxnoCLEOT|gB@aIbE~&C6y)e~h08|J$jkA*6tYHW(<{B#{eM5P-?~ zU8Zx*dj7&yn4gn>{#os;-&wl_D&h&Hm}X0xA0xbl3ME>-OX4#HTsQLvvdJ8{y05-wb4yD|+Ze7>D6RpDjSCy+%#bLgQ1S*h5Sy@ahGAq53k z_bk8NILW%7YT!ps1kDFgVnF!jNQ{V5{&Z0Yl*d*HN!{(@?4st6CVn%w87PmgiEbFf zi8^3V2B%V2IUF7dqPaDIz>aI7lZX7v1SK0U&$#Fqwa;B>MyFiRNK2E2qAW;WXC5+o z#U6b0u-|rLlZCNE@+I5*CinG^|K{rJqgO1&z32PG%P@m4a?8jc$8aYN=IGTvGwy{CuuNDYl?9V<%}!)uf*)AjK&b z!q5yW=*da21}Cdc=4Ho)16?l)#Zn%oFK7=PF%FvXyWzsH)yub8$J9~{)Eij8ex488 ze?L6WK$3ra<84l=sqJlJZ^qIf8%sS-z2V~K-akAHzPVv~e0)^b(J`^J`tILwl)l;# zpYXOF*DF?sJ8%2jeDN@Ui)%IJt$iu};p@674A^qy1fUn8gX#8oNGPCZYyHGP%%7LA zi|{PM9~pFfI9;;4xWi!3m2YC>4x%?|Cu70#xa7iy16HsCI`Td9x8rqR`2KesGowt( zuyK@U{-$)l_&$;~of0YH@(iQ*_CxNH18z%kb&Qy^!xoZ^G;7GGu^aQnS(a`0aRJNy z0k83LxWD8xK2E&L@nDZ@Zp*{Nv$b3<&))q#+Of8^wR%bNKVq-C3nlP8{&(h3Wa{Ci zkh>c%(Z&f=RxLevl>1%`3=EHlQ?jryF+iXPo!~_jkj(!)J?^5ZFGQm}*4|tgT2cGa zeOS4a%yL<@pg>o@()75MNy0nWf~mU#*idqEw#<90c33dsgq7F^i|l%iCQ_{W3CbxA zMO-MKcaK6jH%vTJ?RtFs{?e=v&#suWm#;W>(Ye39)2U7C{wfm+ketFk>-i9FBK%4h zI3V!S>2hb{$y<+4>W(TcTZcSp`nZ<1rv;I>Lv$YZ$YWZfY)0akPDx3r zSu1t$0L}aUl^6R0G9lq^TBR+pVPv)SNLi5e3cgOSmuQ9suzwcsEXYqlHCrQennmewi@GT~Q z@zr|OP|SHL_^ao!nIM=T=4EFO5kFbh3ydX-_a^c{GJ2*ES(WV=)UZl*+3$g>)oL49 zTW14tM==!vAhujqrf!8rKoBQ#M@3^jX8&wBR_``&|M$;fAg5%e__KN8EHOfN0jvGCa$6c@D>s8}|YjO9-XXbJkOzkmr%tn2Cm|;)8v8nM9#Zh4At`T{= zUx;ePQeR9x!fr1W)P#(e7=9T%g8-G=fF9)-P-ULhHT`1G`Ln`=3G=iDa?4gMFrB!7 z*#RthpA=&|#5p4hM?2aaD`&S6{3?t!vj!8qq7if2;FjkW-6(am9aDuuVs%cY9Q$}e zFFK2nw$bnD>J!iJ2E(sE28Uy4t=@xsC;)?~#e!uvxrtiDf&q(wvnJBHb##5tjqjbF zGs)e1lN2j5T@1`H-a3B8`8Q`}#vHL_w@3Ud1nQjKloFKUs;L0u)qnx!CZP!g6z8F6K1f@PfPAznip zQ7r#&1c#1Ed)@@M)G;7GUh3E^on^T(tt{eV=D2tvrV^1xn3jKPWGC^`+?@)=lgJiP zaO%lMw?Ip~!;y`Y9?8J)u)7zpDju;y#9_Bd_4oL?LU%cTsM91ywPcG{$M0|U67ot@Hha*YiQ{bs%I(Hnl=G?3_P`hAQX%~o`>ZvhLq zspBPC7{XJL+8=vJju*U!7A;!t`cL*8wT`Huw~)D8gS^R>YN?&J6-l zT`tet!+-O5+y>jWMmg2auQHZ!?|)W-gD``N4tR) zoIa@pCAPFYsO!^Qq1Hw!8grnE(5Gvb%7nG#;@kmVr-iboWP@qxx-Rd@0 z&HBdUWSUbZm#e@%n5M_4@8gs5@!`X+XWQuOkaLWvdz}PigS~PY5Uqpk;+FXTb~yi= z_wxG+GZvfoQ9d(NIvU)omo0D5B&pC(h556bK5M+j)tJuZP@ujx%)(bVU63o6ynxcp z_s5b+ykSJ~dA8Mpv+szjlvYDSBL=|@pSy~ImR8h$sy$Kf<5cbadDIUr`@;t=3fVyzgo1~ahjdS5}+ej{s-*@2k6&r(IJN_wHvtPF^%qbSI zD-HRV?OcscMordis&(2@<;ZtgEjv2a%Uve0W{Ngxh7nPDiJ7AI>LD6$Ar+a&v$x;1 z6%u5Ya5dBtK_HK=Do3`)D4HK4Im%|suniQ2)a>Qci1gG`;+!Gs8i?D|l;zEM$B?-d(ta6T>JT_=bcMC)=S#pnTEKZG3ZFNe7 zNbvl&b&9lOS(^PAxc!wbWuYYN2iS$YfrgnS!V0CnCHi$(SraNgH)}X7U73VN?Fd8SK+D;4D zaaFMe@L0DfmPE*c9uWR2gHoZXlTY|D<;HU0kd2HU(L!%uDH`+rJXSGeHG2 z|JWo(PfYBIyYD&VNxtc7V6T7(jGLnL0Y_Zwm&((!>8ea~GQplRXGvm7xRffV=3HqJd0K?k%rl_`K z7U9+|uTZXfZT@5A6i4p5nl>)285&_NMtv2)fFL~fGGCFD$1M@uL z>2_7U)`xU$;1|r>0D$%YgUXaCNYEDnP@rWc!g$ZKx3ts0cJ*+C9z+ zmTE$TVdL)iCkn|LGk4i*8U;8jJII6)PP}M@BOYQ+Yl(%Av~bCsKPt(8z*RbNCPA2~ zdz@4(6Blp`9gkQ4QuIh=p`D12RoYWzo3H#W@+`#+eu|{tQtVGdCSJK>c@LR8X$CY8As<6{S>!<5StqQe@3(X1Zq9UOR! zgGvX*U3NqGwH!X8(J#jblBW~|bB0VvW|$!p9!nu5Ir|y5yU;9?TM8Zv*$qj5SUMut(JXUa6EYk6dq$JG81b}g2%7R8>a@ew=$ z4K4pZ5+76$<_}|ZtQ%P&^ns3ixp3Vn8rt3)>Gd_=>|`^ zA`<)v-mBk9pn?U_hP|Cny165XoQ?tq(qkJ0pSo{ts$3XhARbE&K98T;?!??U@4@ij zkG^r(+69|Eq)eYXB32HSrbCNbcGzRF2Z?T%&RLIMF^@3+Wcsm9@}`Jb4_H$HL<%MW z4)EPen3|GNRZaDHJm<2qvg-2b6xps`$TT-*4E#SXK+rweM=DE^WvHhIkK85S-uea2 zjT-_6a?gRSnjWQ<7+G3(HKJNo(i`9ioHvOvwxG3w&-Hup7qsgm_&9_jd2;->Oosp2 zJr-c+?FAiHxqyokmONiq)QTDpbfY9i&(8Ehvgmb8zswP$MSi-!UG)x!VcZX ztgWwm(1LE*cMkge;Wm}gKVqF9Z$aUHvhlZZQ?cG}b{4+f4Er8ufzq9(TsFhMjX-Nw z{$wC6ePsv6D;iOXe4+4I+EmUT2xOpSZl!su8n=KwN6+L%?2VS z<=qq_^19A8LjG-Lwb=JK-jlaM03@_~v6*##qtc7M`Y)~j4q)lj>#cRe<2SRjBe?Q8 z|I^s&*7yF73#|FiM2#rM~-0b_i+;ABhzFB4hHBK>ALXKTM5 zoVd&V)^<~v$({cb3bF(VdeC>BaVBs!LYyLYhYcQdhwNr_VTJ6;^wpC;!sg&dlsbZ@ z#lu}Nn_hz3tUAiG)oevYbuQSBEI^&yQaze~t9EfGd-{i$lsab7J$5(fR&-GwMaVbs*^HlJ?5 z<%c6EH6Ic=83dkW82_Nbrj-clWCUJw3)z*lOR`*;&O@n;_N^vmh75cb6e{|$UEVWo zGuOFsX)+^@yTif4ztx~F0A{g#tsUagwe?1q+rnx~s%i}-Ovfp&-{9l{@+KcJ|3rm_ z@no4&^KO?X*CynH!*obvdI_cd?F$qMvS*@KB&Q8f1vcrq}E~(Ak+mRY(aFI%cG&Rq1(gXXJjTHPQkfS7-$;50MN5F3inCb@y zh1YA)4-O7+3G(cZCb58iYj}TM5_9Gdj<@3ro}*T8{|{7RO50o2+q}S!=l9mUvX`o7 zxd&RjN;f~cj2|hB(cg+7ANj!VGD}#=SyR6g28abF5Nw+El0YBS#MwDKwYqt{4<9`9 z8jjj1_dZY#2e(Z}PfUa^c7Fd>`jq?7gfWO6;3-v3UeeltUPvpI;PJHHy*kQEXSx%m z-33GKSbI2kK!Fx=^10LQ6qf!_IDePl2rt@*#AIFoz{V zLffoMUY%zwyx<GY}2-Vzx#7r4;t1koi!PVh!AFn^){D9(y2wRqv z`Fm#;*KU7fsTsFi&&csdO^^3*ed!Dnu4&;87xzM8=FDC17J$j_+yHZ-Lveyi5J;H| z+E;YnL7wMF4+umtN65P@Zo8%~My5=D#^_NgkdOx5lJqXnEUuCAL#lD09k*(6{}hTI zM;_`V$nH-Tp@{@x+^bl!Qa4gpSD*bEdqDlcFFi^6{<@NPP2qPtddr%!Ub0fhakI_x zlXq(Q5q-LoaG;1#3G`*Z#{nid#eOB|X_A2!pq(Q20j!ka6dE=67t^Re$20YQ-hKCr zm_IwqqGe(oJW1D(?7We5Bo6qwF|MjSl4)j~LI(xm6z>U21pk@&xL(n_v9qgdqX}5r z+J^ep$#u`gua8TgY~SFrR~=k7(>ZQa!NyIi=vs=un1*v2TZDpr1ve5% zCC;n)zZ$xGamWkEAy+fwxiP}V(&bq(qZUKZlh_rYLHrj7g^(1!WeSrMLqcTAx)pr4 zqF(3Y;em^3n7^_ni^(894|d#D$z;ckF5#RLQ5dQ{WYUjv2!#Kx$t|_wUoX9Z>)R&KFnPvL@G1~pQfP*4Q^73zO z2oowS^oygKO`D6h+BG;M8T;rzNYoPY0Stp)WZ?v4|FJQVD z{yE3alo3q&i9UwiL8xfvqpz3Js-S;n?n#72Y$u_sP}|5;_1Nu5GMxhw9FVoa z#l>Z0WBaeMYHAe;=?Wq5VOy9vSH-V{7OKWKAfZ(?L_%E-nVRYos`h>wMw=1dxW1lY4Pf zjWu|=Umv;8V`j6axr_LFO#r z47lvvf=NfL28b^e%#I5LP7Z;bQ zfdNr%mF2SFm5YS~6L^_Lp5;*?ZB*YjQ6QoSQ#36O-Nvk;mraMIH0TF8d9Ww>H+62a zd!G7bW;QmF){PPX@G&~d#mj4AXc!n2^dDSz^&Ee}wxzf%0!JQAcsxG5nr>GTI&>^F zXy&JayyCB+5E&BLFf=W!E8$UB0wgk+f8A`zY-|lLxWw?%)a$UN+@%ygB4rzAZ!yD$ zRKjXb!uUd)C%qp?w%TLeQLb17xGn!cRcI#1#0Des(kkD; z?ACpr);+g=J?_N*D(ifMA{qJ3wS0JD&krfJB+mU|UF49oP2hUSHs6L=DJmwfti-hl zUV?5K!FrRhVu>!C^PN|Kw3E?NTXNg2WCc6~YaX{MQq($LZJeIe2&S_32c8-#sAgQ3 z+}xDAfQ09onz#Jbecw_L(_(=>^&Mqn-1Lv+60=zXc{DK3?f_O6-21;*L@J#(b9fSi zg&N6lGBcf=%X`~Q`r5nxo%U^U__dYmCNl8C6{D>P;e0TWg%Zd;gcis2xu{R9N!6j@ zyw}q_mfI7mBmLL=H%-kmHRkOrI5C8F7GqW!`ECtEB2DmE+gO$l*h(5Pf_g|fc0KN} zeGgg$#cZZ%;nz0`C?aBDUNFx-RDyBNgFn_YW+zeN9)W}paCU*2iz;m5{oUKz^67Mm z1_+(+Vi~tYhb%!vi*>xxPcd?_jq5WWJ}fLIBkB8j@>GTTZiTVz{&4sM!%pY$ zW^45PDiYk@)N1qfM70@)p7H^?+)oqn+R`dXs;a8c@b54?0#s1n-;}HOmpb3k*LD|M z8|{9CPS#A9?Mu8Q2)6D?Q<4eyT;kR0M zon#$&K~lDcF1%nw94IC^Pt-ilL}R%#<5R=847FWBu$)Onbm8&z&Sbs*s*mTeoP}rZ zJnLC|PUyrs=EoAms%_1BqA#na6KV6=H=EPZ@=7%YG{Fgg|5?(^G(~=LmCRkDuhHQa z_6WKVVK0lV>-%v1>Be_={yyJTK9eqlf9?tYwPTCXj7H0B<0f_YHlDR}Tg z_;a}TS%OEZirRQ7|1i%<ZGDz?|o9;3?X8L3*i^GEbOrc{LQ>u5TXo0JQACr-dD`YB~6}_fnAj!C?_vMI5 z&9rvkXPoi=#vNcDmdsK4!#LC3d`vis*DULE;x^VZiq)td0K@CO=T`oCc@dI%V6CJU zhafU71QmF&*4fYXDT!rODOXp7cJIH~BbbkIk85iw)!~Yt-^Nc4dYv$OX7~Z|01Hs* z=E9^vq!Lr*-X@d&Vh#2@Q#@Vz^otSr{K`GHzB-3Y>B64oLWq-1&&@r$*q{RhFC;9p zij_G&zkkxQ8g@psF`^{s?KHC$XD5K5y0*qOGD;DS)ra(P(u9@NsPU=QcoF_t@*hAI|1R6hnJ9FDG+3=naK;Qj!IF))oVvaz6Ylt+9!ek0TQ%Ob_9qFo}C z5)vK5-m@5_oo(Ub4+CArR|TP`Jk2YD(VVM~w#L#gQRzrp#Hy1HxhEeAlCMaZmH7c~ zMvP4kk3DeydjC@xpP8GUPs>F+8(*%N2JClTLXl6kK8Wj_3+-MgO$HI_NR|mgQgc@TREF)ps(`<|erXn5hLL zWb*9w>IdSd+7YkD1FIrX#E>+O7-sajf-tTP5#;pKK0m~fu+t~U&yMTb9S=}4#-Cqi zeE;jzG;*CT=eZiIMY)_MRJpw?U}Cw*=MZnMcelZ6Z8o7pxyB_Wi5fvkQx6V|fKtWj zbOZ;iwoNT80ss+fd3o8KJqs82ys4>kaQ+o#DisZu@9PKxuy=2DjRbiirst&>GqLx# zWUcgJ!lf1!ft3r|I+l`CaB_1MY?lLH*|;m%5X-j*gcS~3MqC7O91`#`*}$Ze$<#7! zO|7z(3jy81OgKB*Dc2m+PMHNXZB4zSdcCn+h-?RIH(6jcjdi681{D(6)S)*8oUzDe%xA z?PH}bs}{y!Mm&P$>q~12CyFG$O$>CKdZ6Q-b|($iPo}&cA7uaOqIr;>Ju7J!kz~-A zp}l{ninK5H8gne!2+l8&mgH;<-H}6`Um5kmCsO7rw`$>WJdeSL3ywbh`CHrX5x62W zMGo9EOaQB6-`&;cAL}F!6Ppo*K#7WM)M}~U!W=iOs!>``eVQGE{|G#JwjyY(Qmeh& z*zsY%oz&RPHOs&&F)4Dqag?p>s(SkOHcByKJOfTP-1|n*dwBkeva{(mnGzF)dx>9o zQf&&+NQi$|%e`>6Qsm$FR(mz!-SvI?qvz?pM{3Rw{!{U2ifJ3)sCp~*xR?nm&-)f; z=j#(_u3_%QIgA|~BBau4LGZH)&rw*@4B&<<@Zi8LTw}`{m&{qsTh{lqX@tt`HNFQ4 z#?e+eTyDO!`QpGgSO5_YhaKR+0x&pEwOFl;JK+SH8`J|2Nb=wy$$xU^^EC~9HilsQ zX+)`SB-4-+S1des{8$^0_p^Vavo>e%P}NH3W}8D0L(g+yOiT>&m$WkJAddyplUp#U z!^|sqX|aInG&UW*pW3!23wHOy_L!M3fb!hz@reSG7~5SQgA)^q#pWp{+Z6%|ZC(EUOy6&)k#%B)$zM4~2@nG`ViO-@P^q5N4Q6#ghMt`lLRT?{5WBn2fX zdGxwrhMY1*Rpai^sdzDG#{mG9jF@odEt+s)jdh`e2E~TMAQaI%2cvSpki9z)at8>w zF-*iDO7Pm%Ev#8GtO2_@MGF^!bq5oPnn&Xh|HS81GG&9%t4_h*uGh$MO5G)i%QgEIM4^NmJqtqd)Qz?oq?%BS)_~m;JjiPK3T7>Io zGrbHutR8tJ8H{zJw6tY3)UQaoJ}NExJbng=lq?6znljAhu<$J3g`+@M)*s^B-Y12e z=Y#;LSW;A?sEmfq2HCldSD&?W!RHfN;K9(Tz`` zzH%`XoRtwfTi<)gzWkV`Up}!LoGw3FmV>(UeE2z^ADwkx)}2|nG`voCO#2RzgY z1c?fZ)_Da!8CpXI>Drq|oayC&!H+z-J%Q;I{bKp`(c|nlj59*M&eRt_lH&PgqK)A& zMZ(G|^B09Gyz~}7Zn0y`OavM9iV*z4&Ce?>&eu6>VH+kX19PAxWmr<6biT0wDq?=i z@lO_poe+f%1deeR{`Onqsf*76cdoSYqyvx7YjQ&clV%Nij$j;cepG(7eFS(l$Y?;d zyI%q(g+utSx{q1!9UG?5BlrL8Yj?&0Th$kF=oXRxs^-p6{_lK`q2#&MSh}Hiy z^kPJi$~Ctz;1`S-2d|!=T<`x!v@FgZDgN}`4aqvI9)$GSvBb#3AOqYu{n_c^6+K3Z zg;clkzC}~^-|N15D2V(FiqBVPeh7D~-Hz>?ZEHqm<|3k*i3uAglA~*-=OMIBX&{dl)aBFoI82Rd(&y~3bquFPG&_Eb8)0S2)$2Jd=b;C=V)3%mR}@2=$C+xI zTzg8$aEp90!{dqG=O6w(Gyb=qAU!=40dF@q$lHTJ`hl9nuIBIcb^OiE%|uhl86phv zepA;^w;hL%o|#yEWcKL1N;NFV5Bx#_>goWVl`HAH6qA<7yYrwJw6kNI`tR&7P`YoU zA$pVZ-Gyo{r(AG9~wvq*0#0@obI%KPOv0y!I zFgO6>ya3AESYu>v4jCxIKPJ+nzD1a(0|jwveLa)X#v^Cct*L0;iITVM^`ut%Q(i?- z?P%Z*Aan!pFU|{ROoF`dkdvhjc8lta1>4p_?Y7z$z@*5?#5A2klSV@M!$9O?Z?oF*gby@Wx#<>_^q55_dGN#8sh+%} zt4HF>RDy+%&gHoC$badKb}4gMOcG>gW^VJo?)*x1>y)aN%+~@xksy1fcRnw6<}V~Q zTPS&jpuQ_3!NY1Z+V0^f1cllPUyeV&&jXe%TqhZu35v)}>&Fdqd}7+Dl|cD}A4;``Gf%pw;a;ao8^_eUGSW<4cYw9s#9#|q<`^%n;V0yVyR#n4`BrK~N!3Ai2bH~u*Hhawt zI`~aRT@O;`M26H^m-~ImVoBNpV{eaWVPOHFVl>t_{_=8!yDCw=TKhu>z<-!Vu5U&G zR_pDQ6zK*^9~nukL#o-Kht02DA+)6dZbo)RR7KvzS9WaZFnrD%EEMr#HPD1apBDD0 zwN>gk9I4_?ylng-pMxB;M&9vGwI4Zn4kP!{%;*=TbW@5Z*~Lk&@_28mgx`QW+`z{> zIy;X6#SK{b&MikO5zic6?|onLa7bYhooPSZ{9%Fx3Q7R`rjw52FkigOMtD0&#E{F2 z=du0q!P{r=*fq0w@9q6L{f}xZSG<&-j!vAU!!t-ME>}Og923Z%Lkdu^<_zC9<$d4* z@2c$gLEGNZq&GQW@~f>DwAdv_~0k2pYxh~Ni4P3=~5$GkS{Wpwy!3mQBxV6 z2V-3t1-BJfEI0oBh_kTEY0@(RB8uFE-I12<^Al^ms&8x@0N7UQ>gj3KC8!psg|n%cz-{l*{(d6Bn=36X8_8i} zW4?)JiDe^NWbp3^3TWf<+0|IsihAgNbSC=hh33jmBr}eHW0(26YUCb72EMd&T?3w< z8}Ta1-{ckc9&)%eR&)TgA8_|gXKP^ScP~2aMBn3Dv}k6BN}~&KP>P|4+#BAxd*;wL zIoWiSjUL*y{?^ddg*!zkKnO-LYD_+SG+CvU{F(*T6%>pSIdqRo+#2+;R$pDR-Ri*{ zD%@FomZjv)9FZ6<GCjsqhj2HoKAIc(|=jdsZ#3#LB& zpj71Yn?Rqu-4d2F#1y~Z1rM+$fY^=N@#!BC!*Q&GOkGI@!~&+3qkn@KT|OVFR;{(y z*Ox0@UM-`eZQggxJKi_&Y8l#>$kg63=J87_b{Gb_MU8SfU(^7{^O^$txZ5! z(ScDa28O+8R%u5)9~%uttgqdynWH00{XGELx7!r>_iO)Hyj11sadJqx*#r2G@o|N< z07@PWVBK8X*chyO4oUhSn$9vPuC80c5G;7G;O_1OcM0xpA-KD1@Zc77aCe8`7Tn$4 zT?W^C-mmIb%|D=u8isw=?$zB7+3A8wWJ_TSjca>7t@e5UyhP}_(MnkN>ke7|k=b8! zd&kZk2L8dp;<_eDll&k7c$B|Z?~7KP4c@hlTxT!AIdlmGixE0}81!E$RP#2KTRKe# z=^uQSc)nf-k`N16ZN}YIS|NX7b0)I3CQX#x0!O?xauIig?J+~vv@=-W0ix7@7dN0< z+oMD2^=2(<3GJL(sSueqPp5GcrO#oi`qjX)Cp|julb1E)qTt_Q>g-pljoCF2G^sNf zjN?<`o0gW9%(>rR+Tvv^S|T$MLgnas@6NTnvb=rs2ldJ{@69FDSFch!>k#OF^cTey zcmLPJ*6#Nfad*dA$AGHv;Lcanb@^OuqTG2ZFi)bWUS7vgOaohz6U+LQSoc$EDi0cm zuDa0gZZ!s{dqEaFG1#%wbgC-L@d^Y+bA~BlNWB6*Twq7F&vtTK#&He|ouz1X`-egw zJVNi+#N^2Gf85MH?oP0}UiYiHz0K07Ck^YHxTLa*&q|$RqDaepcs?P*ZHl~EVvTVN zp44>K@$O*%b7UY~pS^fFgrvJ&zs#bpw2ga~z&{W@i)IVBZG#IENh*Joj$AB>g14k~*U!Ry=F!SPs3D}xf~V< zoVhkVyBacKif|i4eibe|C>Tml-oio^@mlmG@HbdoOp^OQG&axT2Y%r^o_4*T;$Xfn zXTRYBdn7>6wD$w!A835-dqeI6$xQ9%l0l|}CXzf8Y=qSqonY;gnkW=}lzH1R6s)zA zfuujcQ4^N&$-(dtTzVvdO|YEB*NHE_2<$8KM-&l5vcb5acTPoMu8*?}zomK&j|Nx+8~!65ikFVyKHZ;-HT>-# z0U7R9?00>Zyd6YvQc`Nw^P}Z*jnp)=pVtP%>G5 zkmLraq*V@&EN_C=xAiJoEBq$w8U6W38q{|_u+i2P!3 zx?{axNd-wlk#q(W_W~Iv0DzI`?Bz8wH;0+25C%lShFgY9=KeM}0vf!rmmgl#?W70x zI8#KzZ#)nt?bh5e=YUeVZqW&bk!|RPHYCJNu2Gd43Zu&Ak^b-xrKw9-VE0|PRQOZp z7HrL_99eFfJ5htQjDRsnzVd)}C8b!mXSF=$xrIXP$+(pqMqm0mpfDhI8b`3J$}T52IZA_H%bH?mKsHH{ z=<(rhSbv)s;(;>kOS@l^@XLSj*K08>Uc}KmX*QTiiB?E=q)xG8L->M$kSI6p19Nj~4r7t= zTRgR2w}2%UX%6AQAuDVXm!aeJ8t?jsC6Y~LDcnCp^$9CdS%~eT9lL2c(jkLXL#>^j zAUJ+TgPK;iP@AdZ1)KgCnQV41PZ`(_(nfc$_p^VNNI&T80}j=EqZ!bOnB1>h5`9wQ{!$znRlZqnOxJ3@8^`^2lZc~5ig!{xuljq21#5HxR z!IS#KO|WIY)J|-lSDt$TbDnry;b`Z7hhW3!Xc0Cz9_>m6?E*7-bURr*Wouo)k76Y#p&i8rZ`js9FVL7!qQ3 zm4zE6K`A>;8s*59ysQ>ZF(oXB-@h^rU(1kCXBOMW>2RUp!XJr$wlN(`1lb&yJEz0! z>KC+Nq+-m6O=l4rkaq|RVPLw=O+!jXyjPTVh~WdCX|33vZt6jPrD-3!m8z;fDUmJSv^H6f|4TA6{5_U^sn;y zS!Yp>qvt<@UY5h0w?-()O3j=T`aFZj)3lt1z&|Bx8_Vd%gveCAkf~$cF!TF6Q^zb( zioGFKJ#KNP@rcOqYPY$d|Gp26&;8oRE9vG(ek{-0vXP@}eC49Qu1_VR4 zK2^DCg@NAw-R>d*!R53KCy)OdFf#J*ufZL9xP9OPH#3py#r=XsBQ2C+JXb0_Z)jjiQ!&AmpDS4Yni>ID0dtn&3Y)Xrob zTziklW8aI{D-&=EF1~MWr2!l~8 zNGDGr8RfxTXN1Pql{E))4s`_e(O1f@qe`4SF07Qhcn8yAEV*`tev6f=R#idz16`dRFOX=Ro>MG>7kzD}6&J zAG`HI#5@mPZ={<(;Bers8UsDo=rvIzw6#9^qb9A;d1c?X-T*Tw^{J#x=$N+*mSjPv zbsgpWUdu=EkK649=Mk#7dhP1rMu#_00ha+7$br6I*+?(}At5U}JN+F;?s!PRw9YB1 zD5lKrOg!4h_w(fKtt}QaKf^A+{|wuAUYWqgtGb!7$csSpM1?^8x>$K~gf zN3jA7T=KdpE;Ey#8st@KVK1?8B6oPZoshv61WD*fSW-R?m_@iZDN{;csHEo1WKD zEE-=plhCfh&%c38lX-=AE%RGO267j}SQ$zin~bX1nQq+rq>-1(g`k)%PGwGCl;0$& z_fJF?>0^)un1biE{Xk-~gDgVzysq@f=k5-cE(v01+ko+6mTVD85f?=bo(ze~N3(xg zt?VlnLIyE%x({~+rS2zj)2|{Z)aJ!JJYQTiRd+YMH72Tz=pGEK_m$n0=k=`yc72&9 zyoMx8I3SPtFC(R)zXhxUE#I{OZSm}jqjMD#_Nxt8?Nt%RY$gl&c&GnX_E6ZvK9 z^5wgta@fW{SH=e*LrQ68u7_)#>r=Aa9eG`cRc4C*Cr+UF;iUAoE4S56>giNja7|b6 zqmt%f?fLP3C>Q@QnZ0 zLhN46lfBRj^nXen@PKOV4@e9`8Ly>E;bOWz+91#JdrNZGoK#MxT;!@!0Rsjp+!3#d zg;T~65ov?8s`0}NwQ8?6h5C)XqrGrc!PfwT;=%D)JEs+)KVn$2JOrYZOekI8q_m;( z6{HwGLf|hg56J2PfZ?USyxzx66JfV{vt=Ua1y!q~eWX!0#na&dlHOdC`=iH`mzsKv zal%OUD9i2dZ4c$ML|8Fuw)4R&&&}G;dJu!E?oVK;3qK342Rb)k4C(7nU;WGB&e=pjl8`pwA4to29Qd9?XOH-S z=4U7wCudX0sjU=?F{pHuCf@e@K}0`RSUpaFi?2C=HO9dHd;Ky5!JriN-yRYgY{2^C z#oY$2$2#CJCs4$|Kf62sjP6)e=n3j0=S*qTiJpl75UL8tGD>*bWF)=7 zFN$d3lRq?zzhv+Rw4mUwfOt`sz*8tVr7F~bA#}UuM}_(zmc$(Ga2;OP^Pj1nIxTYo zhGJ>9^N0l<>z~_aLJz?G78oDM<`*N+ljLGvB}@6Ak#uYrFDw1ZU=4p~Fn=TfF5^ew z%@9sD=d`5AG{p>2ft7z{(DxqL`=5Jwm-vHwEf#ai-mVka{qsbbD=7aZL9MuBsQU^u zB>achLM7tgn$928GUn!88Z?v6RG18@OJDvhh@94w!ZfmIU4wc|^S%SDZUAeZgt8Jq z{n^~?2<^))F?o7`rYEJ(gNS&@0_r&&oSb?A2^WYm0GTwh%!`8iyxHS{fPXrp)6!+c zw0a_T&OqVFxEsBR0D;62>iwwUM>_G_7Mx3)eYaLoaS{ESHv!kEg9L6*l3g=?Yu#uo zR!F7S5w=SH;_3DM`PaVfIAC5|1Kl2K>*z$tQH}3;Tx#p-g*#`N@&Ie$Us?1)6}Y(i ze|p&St|&v8d0c28XAYT11M3)$T>doy#!~0w9T+pSBB_OQzzGBFVg{lT-`nuQk3bMJ zYHZchM8w4ctfao*%TO+kw^PqYLSM2=Z?%y&Z-Qx96` zDC$>UNK=1ppG!|5nykYcufP>y-yg#*rlZ3h7!kE<`XrBKnRb(F=^i z1->C-%RI!&g?{!p1!mrXIU%?C4>9zM)ey;Yh&6q^tw3`CFiJ2mFx0rWQ^TqBON2*C zqM9;?7fj@m#G@a1BG{Z!WbxLPvuXIedU#N1n@B{W|D+C6`&!`lGDuizWHDzsXMGzc z271603#*4&-z7gVGoOwMd5D>vMOn~Uxx@8Lk9e)Ss@>Nfo$~MgR}oHj@6@&61u|p5 zGBQEO5+=xDpczQBuf$-iNG7T5wvuv221U>CsfjR@E?8=M{OO;k_mc{LCO7THSMy>w zbJ{Tq;|99rOoTXlRz z!$+a0Oh#A_SM`-&W-&6l6N2?Mlsg41KSW zvF-G>hEo(h?&sMj3~{QBLQR)JxzG)7?N*LUeC=n;dYp0 z6rw<)w@C$|X>XXjdhGCYU~1(lX}}Gb{R)SWq%tw&>;RnEo~PtSiL;M~aQ?=RyIwlx zEdJfLs#>0068We`kA=(YVQTbl{Z>!-&AT&uS?2hs{ih#@oG+DJD*kS@!n~$hD#-W) z;H`@kz0JR9U8}+;Z5ZA7FlzENuZv<#{Z-FtH_xfw_b_xiKclgZk%W*=)ewR! zuhGLTe)kO}^0e${(u9-kN=ZzcO7ezO=5w~^n^?APCf_i6;Re0-LIGg#(@?ENm#x%@ zO$+`m<%~Xsn?Ww)sbZb8=J)Zz{y@Z2X*)N(ddrGPNpl#j0uE zp&C9HYue(x+zpylbB*kb+Lu@$Sf{caRLy@?X&7K8+U@i?{9H& zeqHtlEv6Ym@5>h73WRH=Z!T|Fj`Avo-OiTS@bT@n^-{lWm69R()ailZ^QH=pQ@uJa zKxj&mEIH6*l_y`nE6C!6LW^_*h6ZhY{r}ea8s$kpo8K5=k}z7=iL}#ySo=*W%7Z3?{)?`D2RnglmY!`i+YCs`@EjlCzs2fgld z{TX4?VgXsEX!AOB|FWY}pqHg!-j33c@%}uTASAJ#^`67b5dq!lqBxx-sTC)Ij zDAj}?tV13l3(C?oRcqWAsyckyx;3zW1|*qBK1=i~&3tI8t~rBas7S z1@AT}70qsHhegXd(x)ZVG@%iv|65UQx{|9NM0pVI#u!)bY4_Yz48n?f8whd8E1$i`Uk(uM6^-M zqSdr)tnxEH8=uO{Ejh@$-Y9NdV(1S@W(|_d4Sioe(PB=mF%MI5h zNzSTVBh*VjQC3K#1sTJJM$y}5qPMYYo0qXTH@uD)7=2&g&(+E;K71rZ^S$0$~X4dLk@F( zI3giUr|Gxg zr(jfR4uyQ(sb6jP2Y-SPzU#9+pnXpbW%kwk?DE3#`PLHU8!C_M0kmflhR3ft)@};!Pa2Td z``{FdN*=#6ZXM}(hPJdq=?LOr52Cd$+sr0j zS_}p>;*1AsW$(n*@WPpJ_8som&OHIzb``TV?uRD3Sly35ipt#{90Oo+D}yuwo}@e> zq8w!)_0E1aYa)laI2?th$@qBSIU-?gLibyh3@ekebRCR|k6#U>?%~S%BV`*2c~TBN z4m`7*cL!W;PA`$^-#s3KnuXjMca+YxuzONfMx%1K)sFzG~d}guUap7fa;MFZIV#q7; z(WW;yqxcxFlkgELncIqE_1*4;QI)Ia7QH)m^KMurr0Np`lCUif+x)rJpLcmb_Ojm* z6Pw9wul#}1HFx6$ymEE8)%*F^*H8A}xR#bzzhxr>4Ps6&mgV!g%QmW*FqY|G`bmv2 zd30^f>|p7=STp6M6EIKVCH(ftVD7xurH^H7V`rY2BLdyp&JC=aSI-E}Qr$kqSuAY# zK<00!Dl`KR(|=&!D-&BC?LIWb1%tsempWO7*m(TsAo2`s6Ok1^f8f%> zHet=fEJjYr5DnFvK3Z%V0D%q7!feJ8)wF(L z|Hi^p>A;v^0wqqDd_%e^FNAF_&YWf@EbgA-FzE7-Z(gND5(CKMxX}f1p!-n}{MFF} ze4whuaz%orXnN6qeS2LV`;X4i3Zpq&lAr2WfZz)ejd`w|S98#~B$tU$Jtv<`glcIN z1^OUYgi-WuZ)P+MOZp|vffS<<`8Qs}cQ=S_&0^2Mq}v$U0Ni=aiS32ErT134|RnFBDW z!zI@#Wu@V_zllRRIc!_IeYr26oUVFfJ@TEX^o&M2Hifi|S~&(StHznIX4c@uglOD;FaaQKTz7gd7iu{TzMlCxf zf?UX%{dOj6=XWX4F6{V1O-A|}ne8_a9c#~`Fu-BW8KJgf>}LdsBE#i(-+)gFpwgRH zXYC)a7gsS(8VV_Xp}7x(t1#dnUg^UIwK_z+7fhxh76U87wGP*8ugaeLcP8*U&w%s* z#rdoaXShJNW!BYEG6;MUmZI%_6FA)yo;#Nmtxsd&NfxG3ayfP>Jl z*F13V@oyB4dEMKY-ubM3gA@TzuiY&<%stEL;)I84IG83ziDrj{0?8*LZ{U8LNrpZ` zcv}MUn_#;_uJeTt?J$_w=aedgZ&gmLf8m)o+Ae6S8Vy?TeCm)y8uv1m$L$QKtmw$q z>kR4Ph;vnLmbSU8ef;jB8edqlZRanwLgrRRb!YCn4H(sLmpJbeC5JG~$LZk*;@0)f z6=Sx~3qmtd`_}un2hYp=hbbNK>#=h^tCvToVh2<1JIm*5sQoV+PMOSW{3)!=DyJ)z zj^BxVG2=z}qx9$YGz>Bx;pilir+@ zO^0sUqoUcT`|)(fNIV7`12#8V$}nL*g~rF4sb@-&o88Ba9wU${urgDGrD9Vp zJ}IESz=RkWz(48n_RLwVkmp7LVrsCD@vFgDgww_=-XH2DdO`+KGLq)FbU`i(Y0xqNl$Km>6NOU4Q9?~%} z!OTg+Z!&UjlLc6qcE6_x?wDg2-Z6%;zu}^;dUsv|hamNr$y(HiH7q-r)YA#VCpl$I zhOy*?1*trgfzqW-AqSM~XomBP3!r;Ozd;ie3mgFDDzV}xquN&cfQqrsIl}=YPf-Kx znnwW}T4Yg89_Oy9c3q_Ealq24C8II3yeg2Y(9-6S>@dimw^BQE_Mu9I zFXd|2h?XWRlT>4{l~A(Qr%~lAa0t0BN-ULKVu}c4Kb%ySpMZ;O(7ps)6psp$O~f9m z6X1gd7Jn}-y6&$fi<)`od8Ih5uTv*h# zUtV99gzQMuzP>m&LRy{~jN@y@e=KiIAutpOk*K#(kkDo?nmIs7QonF?N(oJeF0y6W zWCQ>;LOowUM!=uOs1{2iC!US`84f~KT=?|s_>JA&!)T&H6!D}utXQMdP?GUTZJAp* z@7>+)J#w5DnX3eUyD%)2ev#2gGtnv(qD7Fxjth^G<5v5n@+(&*sU(E1`UfatrWP{t zM&m}a;pHTrwP2VAtR|Dy5#{I@#}b-ed4~oaN>wD`w zha)g+5f_zvam8c24~28lv904U=1sqHs+3Fe{aC$ZRiIW1*Ygk_>iIjlvd%eSz$4FC zvMnLF>EoB8*y)>iz8x0`8g$Qsbl45D)}s5#7qy%nWyyIGHDJsUAETYt;x#9s4)JZC#?F=uzIHsktG0KGJ&hpY;?8|6f83(Byl$Ib_&a82 zY_<+pD%v~l_Tk5T)m^tgdvsKp?UG4?9-oz(RDwOa;t#iGRaPVa99p28AW zT}Wm6o;U``>aB04$*R?cHEEyqUR zR`A-s?pNgf4YX7A?7UA}@930m?Q+al`&aL)a*l6}Giq&y#py!T)0ISFo$-BG(eGxK z{O|?e|Lw&@d*on&3vlrKbF=|bmZp(#qWLW=dptaJ9bL}U^2)S5t8YRsEiFA3Is87Ltll!4= zytaaGx6ZtoD3<9n790T2fO6Dn!~a9TZu3192zmlvShovPC_WuP3fD1cTOUxFvQ=)k z;(()of0^#ng*Hk#z6vO2HfS^V10NtOV=8}>UI3<49s;F6qkAdZe*2Gmd+_?DUrN`D z`$v`7$GOq54wq(6jL^%QAti;<0`_$#seSdOCsE??KlOrYqIs}3T#;Ay{biX<=G$4@ z%<{dzZmgxJ=P+i(zhRA4!KA}cJ-^e@l+&@X@6dFU&O`!Ux3atTc6Kp9*B}5P=`dKt z{QwY^z^%Sz(uy&VKt;fBfE$i9e>%KKxqm$Rxm10voHsJy_lSaUkUg1QQUBW8s5v?c zDk{J?;$B@{1r(DVEt@)^(Ca_mb6Q&N=A|ly@q5qZ8_Jzzv<~CW-f%s%WqND>UvVPY zLgbY2N>}(JUXNv7wwpH9FRT+GK3xn$ZUwuwoWGXcp4z;Gs6#iy z{73wo3CWNqo4it%RQ|NXKpHHcs^d-M~Y2bO4%^NClW?E!nN%U!HeNFY@PG9MJ^=E++2%;f*GH5QN?mo<n{yV%Lxj?L@!7q#vKQa0wrHbZzu)EF|f}IGBtCBL!2Uy=Qc&7&e){i zbtB&)N7~i)izd*}$3*OV3gvg5Sohv5wfPp{b)@{o(Sw@;DD-D~1MvI~6IleBAMVKwHuxP)C^YM9JEY1y=zl z>czw0dzq@0V`h)bcgC0&!_qXT-F)k#+MIW7UQ%-7<5zUWPS4(BS?YKbtymqL{Put# zzigc4fI2;l0?44AfN6_=V@@eKJoJ(`F5-HTxN>3WrBl7}L}_LJ{`i2Yn$rljZcVKMf_u~w zS94{Wp9n>N2xzXjy|>$+^qokR9Zu_+wL2PQzPtqc1y_zR#eYeomoShNNeX+a3ZM$f zO>13aFR0PUvUXw|F{^aTxV8{ar`%Dp?Dt^sIg`u}QK7;s!781QrV=|bN=qe#j-R8c z-O^~JYfNjYdsyagkJt(q%WT|zgF3soNTB$gs4wVjhSgRo+wbhUhgUdlerUNZXdmxV zX~fS2mf90ob;E*Q2~H$1u$-zwm*CtAc>{^;KDdoot#{p9`$y*ueNUj}LV%dLD0I{z zYlN807IIf8;&l0-159{dfReAl*pHD-&xf}%<<^!KiMDm8(-ajVRN@VDH?z?9A0#c@ z&dx5pwt*xrSQClAF>{gnQc1!epHHiw_7WA5&P`=-2BZ=i1t`xcx=nG7G-X)ROQxb|uTXH6b*4P;7!5a@$&)du-&P1|+w;TZp+e zJ_9o|pH$A~Wqob!HbJn^bH32df0gd`1JbybFS|nT#_xNI@4*bCf8EaU+%MK3w|bxc zuo0hTewT4=*XTDqJDp#H#Xq0YX@h=pkGAvHI;<)~Fr75E4qI|w?mBNdQuj@n zb#4EG*a8}hw0zb35X_r}fJ><;YU9_mmKGtJKaL~2G>n5}ts^{?Og0s{d$r-SDtT&b zSB&UB(ASNP;x!h(C9!{Yn_CXJq_g26m&~7P1tU5)k$i@+WWyv9x`OZhj4*z7#-N?- z$iiPRZH2yQoTnqM7Af8=823+A==M6ln;Xh0Y!!diL|UdpJ8?oe{ZMh?XnY*6t5ld0 z#(*>k3)u)Wk3Zz_rAi~RxL^rwgv=gRKj(+>t$|o86vp;K2%k-74oIsT66^kkP;^-^#9EuT4YX zi%927lhL%G@$!E3s5DTMLb`+dZ|%NXeTw!}OABty?)j#_SJ)&l{j|Ls9O9P~85s2D zJHwH`Nvu@;ji$j2z>n@1O878C6sDSyRViW4zdVfL)Gd}Pun{Y!xeZVw$(tT^Cl<)7 zYi}{AoLcyps^8=(9g#W1U*C{-X~`r;dGB`Pzqf%jTS7{ogV4?A*x@5?($`Whw>x7A z5m)&03(IJ9ujIId)5I1WIgkqwr(kxcgVU99dp?UU;lyy;a`{*f1koL{m{LsoLTxr@ z-~HgNAb?^hOQm_;Xl+nL#A^W_3-c+5Xh_Q z+v|RUIHT{KA_$T-H9Xo#Pc#yzg8`~)d|6bBPGSvFe0rcYpay$vd6T}h!MS8GUigAj zRm~O99(UHx6~k^@^-=D{OE2b6256CS8pFAeiia;_6}0drkj9j)-sLK z5BNJDQiIN+fa;1C?^D5-)fb=F4%9S0uF-GVGS_{(*c#zGaZ zT(4t7OZKM^c~qJ{h+5Y9s(Ut~A@$2+t?HU;siJ?^OZ%};JZR$N6nb$6&AKP;OfM4y81inu$=$R4a05`*4}fLF9+9}hW%y~!+Z5uNkiXkRZdj!+tC#3?Mu1iPOwt2y(Hl!%4!{_JMmR9$!mY+bYGZfR+0+gDcs zeSLzBI%i1CnlCg!PtxJSXUJ!0 zP@Pj_6QfOmB<)}KLwtapM`zLOLwCh#KB>PoSDJ`^t(+BwPmz+V5t;eR>Y=F&t);p7 z@Wg>Tovtft=7Vm|4;TyA*Vhe?eO}cG(1(xXH-Vw6QD>Zz0ZwG->{S?sEod6z?{1n3uHg=qSwGhmNLcU za$oociljx)tOsV3wtsYAY!HQo)g{MDlK(d$b93(j`?a?>sE3F5cgU9)IH0hqrny@zw2pE<|kzeYrXRMyv>vjmKb9|J=L*JAE%N|VEOjV9Q{f+YS1`> ze?KvHQ_51F01nzHQ&<}>x{eAw9juQl2YdW)DKaS$?~AQxJ^WB03A(j59o9tQNRvw* z6NhJft(V&$HlDcrLw5WmNLdOSoFle&lI-4tk{nw$y+03LNeX89ocH$o%dMYpOz>iX z={tPTygFZ-$1Tc)#c4jY@)|RgG_W6LZikly-n5QexImyNRJA&4J317jym+lh4>E|> zT6k%yWO#5|D)Zav?VlF-8-5MjUaIAkN#xDvg=T`EIY~|kOkA*J%{wH{51{1dUEH$R zLju%QV1zQT1)=Oh4`;yUG7l;~31Le7UR%8KO3&2w;hT4+JD1dc;5x z9taJM7+@AJU9|F$QLgTvu^Z@a++7jOP;W{&#QMGQ2{q!=zU6SP!xPa~l6ga{N(-7t ziPID#FYPq_q{=6K6d6}AZ^Y0N(G*v%ZV<4Yz@D0_Ejj422x55DLeKG4RsJlcxB5=6 z$LU%qzMx3Pp>l1ItLGQCkj{~|^MdAS^wLt1BCH;-uxz~*_vPHAi7{jfCt$)6Ttfz{ zquZJiMfxpPT|`Ch@69$A|Niy6`^Ij$q0jgLyK)VJ7#f7d#Kg#7QUF5d+O%PF{XTe! zAEtG%@0Agz;L}Yji|v~9jGFg zfHeMDU`8db9M|D&kInndcj2OX+moo6DU0m0kCTv;Wj{;W7Y|77Djwi!(FW)3qDu%1 z!>?s0fpt1y=1%7Aq~m^^K^{{eSa*C>;eeWS|ea%F!aQ24b7xDui+5 zhtB1&35U0JiW)3R<^N{;RyZJK=Y%M*>2B+S2{R9%1SJdHvxTT*tU=t$Gm&}U5tg;7 zdmnuLZu5!jy|y`o-i{TYF5J6p1vPhv2Y4OD@-F&%;?EdoB$MX?GdF;1`Z6TR?KGOqm zA)!B(`R>R0i!vtuVYf=hxgcHFDgo5*hCh>Np~*z#Zass<(&Bw8b&a76^`TAXWpgPZ z4?d>k1Vl}bxiIdGwqqJ0%~pk-h6^;nI#H_Sh*WZBC0_&4ve|gXN7UXe9!Ra{e2SIK z9|YVE|4qdodmqn2+TPdc#)D>nhT`lE(_JW=;_>Fwn=eZ#HA01DwpE%?b|$oThb z>+Ac*&X76`G>^KeWPnbDXpWk*d7xHDFoF8+{(N;LH87DnWoaeh&WtT{$CD^BGIHXn zOwzM#((JsCVRXz3jhFg#yDviWVF=z#)GU%+s9>~dnG;LUitrQVf&NE_`H$z8_Y zRzpFj8}AoKpH9UmqDphw20W8c6XN>+Y6zhxqs%K1#Q9A51ni$w~czAFdk?H)7`Z zLMpSm?5C)pulRkWwB1gV6W7nP7-y^ClFgI@4?}{q zk>zKCoMmG=^j*5hVx=%dQbK)l?@vTZZMvi!3(=>#m~5KgL-pziQaUH`H_^2WphMtl zw)-L&78Yjrg_)7@I#JO-=1?1IxvA6=GT*K+s_#cKEPFVzRZe9>G=o)sE=hw*pD^93 zcXEP}WEh)45y`IUje9QEc!W!)tIv*3^t6W@4s;ATgEE{5aZzX|Ha3qDX5v91q}^%o zcXjtwuGXlZ%i&pKubiE-G^o++rZhHHP0<}fr_dI9XfnIsS89z9RiD~QMO2PvQbkYp zw5VkccY?JFGev_g)oHxk#Odh~bviXtJ83e}q5y%ZXC9WNzs_ZQ`(=xwxN%Gj-=+4w z=A6$q!Kv^KnN4pgt#Y&^(zz_R0Qc*@YfUz7=A3S}^mu$Zu^w5(&(7zk#rB73xV6_K zD!kZgp_hcbfWAzzuvd9145~hOAOth`Kn%WM#X(xTY*bR~!Hb5~-6j21T+)mWiqU;N zGEb;iK{Xb9z6kTNdx!n&1SX?}iM1yb_&DiTm7{_m^kP4Yg2wW4GLPOZA57Igx;B#s zg;Zbi@9m;iftOP@A+B!bb*tL_xV^%zaG2%i5$5XQv844Qo)N8 z7#@(&EDBUdWQTJj@B^^JZT>HU?P2w~=RS^9+NQB>{-=V6RayD7Hmu{SyLlRb%PZgi<+t&P=(HD^1V?~aZwc>BE9Gq*fV!fGQqdBhZCM^bZvt9dDod}#(Oq?X@`+aN&ye{@@^1uIy;mHR0x~FTgn>Ok@j9)|*cP{ctdVN1rxB zI`Ye}ab|A!JAw`k?8}Hr0LhFf__T*{vC%RCZ4g) z|BsrUHGr`)B2#w*t-dYqMLqsH#TX`xzps^1SNT@3X5iFjW;{P#ah*16!J+>_Qy3Mf zYL>8ENCAXO-QsahAuKc5(+cFMXv?YjDmpxV9eUob=qC^?7y{Ftq9Ca(VrwQZJNw|$ zQk-i!AYIfobRP^LPGooe%j9#v^3C3SuH4-Dj~MiHQl33+$Ncy3tG2a7@hm9|S3n_? zewQC1{#|W+^m1wmVEsbx*5fmka5Xx%#{vO>f zyt%6xtc#ezb9w*hB9uhB{eABE>7r<;s2}?b`>jU*-wQBl|0jPIN5)CM`X-OfqSdn> zD-22zW=@BQ&%XJ%LB~Wp;t=NXYtMu448{JBj{uSciW8QrX^HN!5!UB7$iD3<8iPGa zru}cW=9yv9U3N z>+n&rxR+s%9jde`8y4XLiN8?wajo%zm47QSIdtzv25d3@I%DW7T`Z483}F>A+D^%< z>mUpXB}B_uTr%{iZNJ#oWL{eC-!WL4bBOK<0`EPrUd4uFF1Uam5E9C)ZdlU z#kpuV@azj;>(o`#%~aD3{*Bw~A1)LdI^CaG*8PfF$5vceT3yYbwXCnH@hA`i4p-oD z3v7QLyvpaDF6}!uOaS~lcC3NJ)3XE$HB>S_q2JZ+B+wWJG8CBLxWXYIn6gQZxwc^2 ze+oL(Yxj(|tBgLx-qYDGr0w$X$S|uue!f_{a}M_V{yuSwSDG)K{?O@lJD4 zZSby{-1^bIu_|Ea^Vr!Ev&^#2%{DDF>lW4q^%XmW4m{l~BJ|ZeAuP`~fDn z_wz~A&0gQ>_0CHUPVkmakVBvO6;cSZ%-@LC=deS@g-$audYW-ArWeW%{x6ZjCA0oCF(Co?x6e7I1X!pT`- z{cjSpA!-5vfNQNkOCDgSdFw-PULvkH^h7}T^;=w0WA*}}LlzrvKN1a3Dt8!{;eqE{ za6{p20c>iGvFJcwm#xe0=KkUN<$vw@mZf>Y!Fv*BmVsCj@Sox%nU48QS5pRTEiqSj z-0HAjBrEB^J+ZU-)iH2JXr+m~o1!YB>lQ;7#8SeM$YWHv)WL}FYUQarF&@A883tt0 zYgXicr0)(Udp^Hn=~(-HJosn(-^RX&1E;EgHu3wZ|G;{iyCtW@M$_zZ+*cO%uI{ZL z*(6s_3(cMV0DA0W%q89Neyq;huac5e}OsfS_F5>R9uVYFcxdiYxGE^`Ao?06PG@W8c-D=4jn30f|$S(*VhHX=B5Po50C| zhKJ$>4sL8BnMc1#a`<%d|7g0#usYvAo>r~3uv{k_Teg<1leTOx+qP|+%dTbHwmY?~ zRsZMr;{U3vSLa;U^PKPfy*~u*UKb2f@xSZq&#LbpuP~C6lO^K=b!vtJ5KsY72=ne9 zy6aN8X6dJ<$2S-`Q?&`)d97CakqK4H8U&&J)l(oW*YkGMX;J4gmTn+@q@>ODiVWD1 z#c63z5nG0V8ezk^VUVW8leSZ1K20E%&3SZ3>>UZB$Ui`?@XyZ)>U%;A2FyX6EY&d| z#@vs98osh`=KCj^=q47l%V@ek<>rH_s%EsvWXx_1@5y+jJ}fk$_+>2{Fby=~>toh7 zw~K%zmSYPCmaFefeq{aH<7j&3BC_n9}P{Mw2OE=IMWs?xzfEn4b!2ikM2N0QeN_U^-cQ z5B`$hOGAbz;;%@Rclpx$`T#S!?pJQ8#3MNgy-@drogZWVB~K=CJpaF)o_{D4PdwLU8MOC67{nXeh>*KgGqaU135!?y`)`Kz zzRBfyVrrKUL@+aZP>C5Z!N)nuowyj6NEX1sEmtj=CI3-)%L@Dj?>bSOOax!dF9}n1 zluQ~slEzdvFG{&!G^z*uuC32jZLol!61C@ZMFbN&N_bj&U*2k9T=Ic1_uG?cXNNom*Z0zO&DWYF=Z*|NI}m zsFy*k-_LR#!{rYWUY~D1zuxy@?5J)n#WFyX-?z-oN@x|s8^E!baJC>JL2C{8&RC*5>vF$^Ew$kyPFD;p=?gm#L! zYkRgag00Odd&5WGrYYj`sl&Ccr{@iNOh#tCT~*xwZSk)A0Q9R|3E`zXSb@{q`cO;g zIK-+l%zfs%);KYv4;MkG^@@7(w?xH)5gTDWkcv>SWE3weRqrpwakx_@!BE4HF-5=G z;oWzkDTJ32O$dvr1WNVK{c7hdD-g;sAbfMjr~5_R#o4SeeSCak8sg;EdM|uj5QT3{ zk-vV<{9QN>=P?h6S}^6qH^lL-`ZOym`n9qiDt*VbpF*lstTsa#Kov=lS5{W?Hmqfg z#70(+Tgm*Iy22|_?Lc#8^FK8;4VjMTAop5Zx9$M{kC!co)8*Q}VOM%HX_T@D;pjWT zocd%w(bQu9#&UL6ld7vzm`Vi#p=8A^mJY4AzsCZ=Xxohp?=X?NK!$yEobmzOwO0F5 zmEqpvw-;GyJj|_F-LCwc1Gub5=~)d z6|;NJm_m*WwW!C^GGY1V=bp-Gg{bvq!`5Av``V0#Z8ArJLx#SEU#0!$W8nSZ?DE8aQ!WxIaByZ0s!?#IVu-;CBeFs`~2vZ;#l)-Z<#Zwx835I2RMJ2X4FV z9&=>K`IF>tngO2s;H{U=t-YMLIiEqG^fYu&t%xy3eC*sy&1X|})4iFv657f^-k(H| zB@In%hB0dndMMcc&U!z$eEtA*H`&;aS7wJ z%7lxq{pQ;ZbmdU`kDd@hbQj2gXPbH9A2F%7J52&Ev{;%+M}N6geV&fOL|L+lSNNJm&9i_!Xc-Ngz?my_B&l85Qx(s2e) z2LlPFp!173IpvaA4!(0%qDWwEt$X`Nc@y#Ja8KBfmQb5_v0@|!9(NkQsL-HhTAtpKwknTKPtYV%`Msq|7BTjI6wCCZUb2+l5L+m>IUK5M=0_MpM%Q?T>TZi;){zwpOks^8~ryOy){lgtnPfn%aX4L%^ z8^sDpNX53}OlZeUlUKIZkIp}r;R`nNQR~}m?e*ZG{^ig4bR=JQrfVTm{1;na=!9L$=wIZkM^q=@4h15{ zhJK^`y}rUqR*$ajs3SAf(PZcMV=Ej~RgzaS{T_i1B2t@Y{z>f@o4Fs~}0n-=Vx0GPZ_Ko7Dvp8eC*EiA{U7O!mo&l|fpOA-?t-cwy zFCO-X?LFoVMWoaW*T>@nA>va-87ZX_%m)IRI((m@&b`t;F1{4u)z1%S@$)A8HytB9v6!gL34!d#M@KP_tx?RMFX@B{eMRSc8 zq-kB>IajhpJOdg1ag#K71H0Mv)08d!uc^x-o)wUls`nuB~$l=t$LA*El%(eF46nxrw{!!A}oG3^o*iSQA{0EI@zd_({yLlf>F z6vPeiD`{yb;F^fed^vv)b9%ESAE47yzE#?CB7vIH4A|9izGeQ`TgjGGdcD355>uMK zeE8TG+sRMM(FMTIdQ5tDc5%%%oF~pf-bFODcY9Z&qb7y40J?hX&Y$w2Pv4Zo;}j0m z7ptkMjb}HGPfkW#V({7k5|LJW{UpY&wmyLQTwdAOJE&MXKZo3yScYr_h~VF)J}~A` zhr?Lh!Ef9np90T!^#)T+4Q>U-v zznUGL$JS6~!PSpII%v)P*;~X)sOqn31wpTL?=Cv7zO z+p{@t)H8VN3BnpEBvI(+G)eqE)BThbVX{zG`gZXm0C>)a0u5nzNg!~FakuSaU(N!ED z@8s)#s$TRQvb##YqGLGCrmnK*%I-FhF`-9}!Wbv^TiHny5Y~E`n(H+Ak|rJfMn}aF zvz=|e|7q{MdEOFKv|yox`5_@}e+*a%0p?jde$|L&=kb)b+(P}(Puph=h_zlOnX+$& zlA-Yky$lfzj?ZhfcUWL&)eFL8Hc-+bnx7R1eNGT(kNlIwc?RrCMDI} zan-}lhn1_+GkGZR^710TpSxHVhLpxqT+T8`v~4cOii9gqN&0*NLD1^*PLm{XTSiQ* zDMet`efx(EG?2sk?p(x1;SgA3l%L*eHZ(3+5RNDT`f}tD|1XTy<8kHw1DnVp0nAby zc~DKdR_#+YnLNQuf3LuQ_hL<5a#Ru8+7CUJzFprD=DAj7Y6B1zwgvN)v-1xhiuL_7 zVqHpFmRWP@`o%)A(L6PypU@aPzZ9r2+-0DrE>h%slQL59s4?=H92Yj!8LPhF!Hs`{ zwNJV^cXqR>NKrFqv!E~+7Xr3sqD}8%V4lEn)3X%sp<6!aUSC{=biZsRi_EDT>-QrC z-242y_Sx>KlF({(3t|dXiU${dP`Knjm~E^GP6q$o_gla8Anu~}s)nr6lHZ?A$&{I7 zETjI-mO@gy*c#3IDo0L+_@{;H zU6Gd}!qi8k9gq0E#%>Ri(>Uv`X|{CAyJ_sxs*rqvGMXT*`+Mhs?<_A-`{g^x>Svfg z$Qh;_bh^Fe3TBwFw)Ac&1k3okx1F3{g}yUx#(dh|p= zA;JYWfaR4;t|XY~D6J_We4${*m+dP}8F*AoXqU@?2{)bM{J)R|VDpyxBBdq3b-d>` zx{!h>QmYW$M=F+=CQrTI@kG?OlM3Vna-U{iY8I?>aw|aVdR}o8d;aZOlkpOy7z4ju z3BGOp{F!cQ;vL58a$#$I*MJ!Xxg>Y;04E>DFsFu&CSAVB>zC0Kj<(W)8>lJqfIMq% zAL}3uuf?bXhOt8HDPF1Rh0uVAF7+QIi}#~-=ZIe`EK-si)ugEl%1ax&yA_((oQ@je zQb`a2;f3h{6s~IcGC=43nZLefF$ZL3QPG(qmJ`4~5a$S>@4~&a+Wc~T;-FWt0~Hl@ zAxoUI)=Ud zkv*ze+`s25tiaDeqZC)-Gn0&!n zHAyG-PyX;yO$%}T&wb^2RZ;r)DE>wS!jr1=6)C;j#Vc8S)r-xT^ehqK-Kxb5+2IYLDTB4JX&;j%cODh+E85&_;!QsxJjvBmGr zPi)s33wVml$LWzYc|Vhe+rQqMpLHWX(**mLF5=F>C6L?Aq~tf%dU zvRa0N!K$kDyC1T{&@7g|r6LwMIVvJ)vfUgcI5kfr9eH3+eLB8d-EXD9Tt#aaJ6X!% zHwFCdCxfcFOj0k&ZQJUZ6gx>Tadu>0c5mVN-!kGyo@(J$@*FTAOeopZqeg0B;}0Fu zq%3R1;HwpN6FyA*vH^qVm0&+BxWT_kMZw`F)jwH-$w3$A=nEF{!~wRk4-XHt;r<#G z!NlUlRgLR$V0f>ToyOXB5f_ta9n7nxt_NrYDfw#lMn&`&FsF&7Pl(O?s)U)s)d{M= z-sl8llt{tMuHvA^gz^G=8ioJR)rR`#@}2md`+dUa zZwUNaZ06^Kl)+xWJ?k^%KZeebN`cRYN%bTUPQJdyHAqavM#69p9@lhWMEZO!YEk z#&q}=@TD;;10s153ybWKBCG3<%^_?MC8~81Fs~QK-mPt^9AgQReF3txyLHMW^bda0 ze>w3jnmf3pvRe^&lgW(hK7V|F_A0{E$Hl!XAI~aLv;2D)H4lk3jZY_MWxZME8)K09 zC?a%5soMjP5{41v&t0bR?Vr5=1`uBpbYFReYn@khz~rkWv`6IY+1PBUn1{q$Grp8Y z9ZyY8wwX?Aq~&+pUuLDbU97i?bYioBh?(Xxjv#AZ-?Dl8BJR}1x4#2b12pQTxGfW0 ze?F1zwpAXaFlLd2n`*-qNC&w$6GFPoXpaHZjlxYmNbddzt$2&og)XrjS)~XIF0$+6 zUs8Qso{=4k+CMwCk`=1{<6~oE$ndZLT?Y6sv|32G&6F^K8CM|Aj{n)ca}&Mj>(tHi z)>g@a1+dXZ{UB8LM*E)qAJTAcg<6%A=WZMkp;R6hIfT;E)6vCJKajsq@% zKn(cvv!#3%k6piCt^E8*5>^Kjg~FFQz7oQr`IxD>v%6&i3fC@xY_$Y-SQZdUj3lFk zQvdyZ6G5{xVTC}=a#x*AgN7W;!QRy5#H(M?fh4@ocMv>s2)Saoc&7=6UW_=P>7ob| z%w{CD6h!WCS5^&})t2zP=dmD&Q|MzSq zalt9~)Tbbbz~HiQt@t@b=VR3vt64law8IK_7Xdn(IQ7E@1t*P=SD!|f5g#dHkZBfG znFwouV(b7{cq(S=Fsn2w9$hpCN_1XFt21jxU0p`#yxH$dH*ap+B;ZE!;P|dTSrlP# zjx~_z^IAjHM)BK4B9X|4trUJ)5oMW560e+tZS7RRX*flH~ z*Q@fTJT0!X!Co?UaJbL#}Lt-u8T(h6k_O2c;M5s)Q(W^sjwF8gp>}&!~Rgx6xx~3C6 z6w>(FCkC_RuqVgsZro!|jgpvxU*t0f|7exWS+c zLKbe`Ns{9^;`^U`ost|$R5^0=2i-&<2yyL$JL-w_z01SNo%{Zc8?d?oobllUHqr+G za=#1Ux%B!+bWqFk!b|6Fuf=QfFhO(>#&Q?XrY-X1nXUD0=y(~i`e^p@J`CjgrWaww zEkSp%*MfpjgSzZhKV%0<-8G9FNt3c@XaCme5Jyk0o&;^pa4m^9&64;p#pjW+g*#_Z zKj1YBzd*S_G*4QLXI0SPasmY-!En9PPBRQSGKz_VJs619Tna0gL`h(slbw=d=4OrB z^F8Bs^&UNp;av<>n(5<$FbRw&>KB^)zNEc~l(uwq`W)Y!AOBnqS)w_N01*=@(yap(6O{u#N%&TOt-A2>Ul5 z7T$#+^*gq?lT0r}%v*n4jic;zX=tQoc9?#{=RH@>Z`$|VlxPBPsXK(IQ$7ZW*|r1T zcK}6Q-cSv4V$9YwIU68Ac|a+HEdJ9%X!bbg}pnmz=uB#wfIK*NvQM+UWml0U-Cwo8CS9`e z{cK9CUM;(8C>-uwmyaswR-y~ShYoMtdYNY9{BYUusiPWZO!Jg;_u9ax;aM@7nwsNN zjH!dMvJx!jx+Yp)f{2i?K1~M9OY|W_?SJWJAn@(&or8=0c{yj-ZAL zJZl4F%&`+DBM0^z=_^(ojmS;XOZK1;St`R16!=ktVFD5Pp}+^8{b6x1G&dg+FO?Ns zI&%Ur7PlLdkb;5&j5C2G=~^aEIU8IOtrX{yzOxPO=nQR^r+oHCi^i6m_#455k>$u} z-6ren739mB9O?x5gt0?&r}maK*;9pC4RYRt5?pv_Aufund)3fy$Wl+`wuv7J+^ctG zeMyqSI@Rn}6PQt=hBVd$L|5Cb9`TmNV5Tqc=65KHIA|Pq+C+tT!=}Zfw2!3p6zrN+XaiZy7PalWNC9&Q6 z4mx>30o$3y?gt?~ocft$B4qeLo>?rhIB=NTYdWwC?HM;TuXMFee?T!bs;*YVOX2tu zA88PTPhnPO%4~@j1Rk@HFX(-0u#8!azoLUlTfv)?V_7LRap1ZJ zt}!MgTfUf7-!N^5S-&;iO+9<-^jy3C+q5}ksOBX@(@Wn|$E1v$ErX*2l%QHNAzZ~Z zrS-V7R&&`znYb7BX^xN%wA&X#Pr-ry+dyZEmR!`LfGn^7#zk~eTy@{f1$vo5`oRIK zBfM8#8gi>aa6=C88%&5NS{TLE^|}9y;V?&spDZ&IHLq0>sa^^xYf7S~kKC6)?2RQs zJ|=otcI%wlzvX(c-ZFLim4q{;JAFAAu5nXhYR1m9&o=Zq@rWl$+Ro`$P@jB)5SN~n zbEH9$9kAP!OLPB?VG?AE+2Vh4{?aWLW@jqir?Uh>EJu*c!(aE+&;p2oCgn+IWI5@eg z>ELK=UGI$oP9HIY6Rooe;h@uGlVz#;B5)XKVp8n9mL|*Y?wqclKu@00wYB9O{RSZ= z(t_cizi+>De!srI3`C2JiGe+4>zgk3cgwabJq>LOg*Iei$))f$)T_+a2e;RHa>=1@7Rg>aCenw%M7}NWkEy4(naxKXH8Fcfh-PSe9|UKV@8NsvStBL zGGLl~=xHX$lv0EmNwiCd#{Fpv9V=h*pRh`C7-;7jK1kdn&F)^F5>px{WIi)u!d;ez z002l6eZokrbWvqpI%#Kxf2lFhl?}<{$SqttQ>s$?rD2E$2gIS?qsFK_tGA1F&QT_e zpaqjkNDMjhXls0>^pCClZ1{Hj;#Iy-uDbJFO`juo)3fl&2uxcS#1{nq;NrY9pTkBV zSGUr@GA=?%PFKXz<~b!omIMymLv#CPO(#Z`LYX!8#X&T8?V>a>n?@QJ;i}NS0bvv^ zYvG-Dwf$_nghkDC6|Hu26DIOulR-EDM+Hd*6C*uprlQN}eM^)KEe-~0)y%%3L>3WY z2Umpdes_?_=P#JnA1LtZ=ffp+T9&W)Ar?IrCfO?Lagfx!B3eWNKI*V!N|7zA^R|ZQ z4|)=>9ren49=J&a??;GfL={8UjEIWZ%PWsWzpnn*)CQ*QJ~)by^Qo5(^eWz@A?%~J zZA?)@zer#}+S`d^VYVX#^bJS1Jp4dc(W`unbG=x}Lh**#TqWlT8!wQ>z*B`1Q2ne` zvXHlQ0<71^O}rWzYs%U9c3`}|KvU(iME+P{2Rdl*4}5fb%M_yZXF0WOizz$vTZPeNdc|o!h&%xQ?5!S z6UbSzHGWSGb?N5h-d$29PJHQs%f;QV;WBoVPCkHiN_BAXw-_O_k|+W#Y&`l$b^#5n ziD3s#JR8_>{ukB)jickn{KMbb?N^nu|D?EJ*`Rfx!3CFCf<&AJYMi^ZE4>Q((G^i8Tg1*UpWVaT6A zLd&K`28mH?1d$2~XkzHPoZW}dGo0&uZ^68sypj7dY^%StB+!EB#LNz^k&1-FR-W+S zg;M%D4W$>*>rOxiBS=qH$!|>CZ&4n=T#dGn04<|0g;<(wq zl3I^a=1kSUIuiJz3VOywMNwl?OEZj5R`4rJhZlXR8YE4tT&;LJ$?@(vKaYgRE=P!t zK`E70v$I>-S~&vL%}r&*r;#yH)&=KyB?tYl?-ej#Na29>u$zU1`5*cnuOnSzgFO4f z5NR~5&)d;EXGI15>YwO^8su#jpCLU8XX6!qjZ!CT-w4`Ka{;;{XGF^dQDa4Fsf{@J z;$Cs7$ki0LkgBR*z~FSfJtjJc<7+Gs!9F}JB_w3!78raDi7{(fjf_$I>lt&B#vLAn zT;_-^nAB)$&5=QwG{VBqUv;D@OQ&#e9A6LzM@bMvtXKh2u8MYexX{i>Hy+$_(uU9C z(V??E3VQM`nArx5e)1)TShbNxo&q_|r}~Fe?<~59oWU9u!oeX*S#(|a&5f3vj2F~bl!UJzp7MnV@NxrGVizYdn!UbeVK zk;<=^JL;Jql2;j3Q;1_t*F9(Uml-Y=uz!eP9n zdJZmf1MkquwZyn;Px@UGJ+Qv4b9Wd?+95uT!E|T*4ErS+leFZi+u$*lFcUXt-`g2s zyX5$m$rq{{_M$}DiHTC?Sqj^OJ2f3qT9vF%0q;#5IdKyvjc|GIRZ;85@);wiZPk=S z<$Nk%QPaj&S`(q|gudO4uV(NA1+@znNl_jIm0B7~J2t=m3=xqeh+`c$(N51> zH~-Yqp+JP^^Jp6Q_m3hCRAu*?t2XZY>0N@gohd*f%5q@C^mX$5>b6gC-%AP)Z5N0 zxnKMnTrx4mcb<60?v}pim$NoEc+9jZi^@D9$LHKI^RlV(0%c)DfjJOH_eQ@huE(R;PO>;6D$tZh#^>93$k$*!MR>{d09 zyU#b1TR1K$FqII^PsseB`nt;b?VTYE1a0Auqkzt)t5t{_GK<@PDqmfJ5)VYl&?O-$ z5p9KAw&CXaHH#qvOV*+AF{_A#YY_T+?6Lzjwz<*~>8d$4 zGAwV+!Y1wxVuClUK8cdpg*<*lmZ2&eYgN(;6xkLvvIxsh_7K9MJTQ$Pnq!-`d!N7_ z>z+Gfkahb981Yf#e44_aHoKCl)KrMUMl9ue={tD$0Rq(-Vk9Fb_0qm1;bzV4n2kNg zTn6y6^ak}|YPg0!uwofhWwbh7lRPpWFqtr>;OO&-hSANIx^*V(ZmYaL3C1 zqk-%TfyP1f9Pej5sJR3~xHvtxCuEYR+qo&|$MgCrAU`9fYG%{w&eic1{iwS7_$?9W z4v(xiO;Kv(Q6bKtk-@c?LGLu~$P&J0^o#R3d0z4sd~!R$#l&T2B2`%h!F5=i|9wMU zRLFEF-_8qoYLc;W(8+4iM0 zcEJpQ%q~%yvf|#U4$9Q%odfceBj}mZBh$F_rm!IJ{>0P(0h-cDTeYI4i-b8cRHbvQ z!L%0?C;Z`L#l^*yzvj>Gq>4O&^o>$ALl&QcnL@?k{7Kai5@ZG%`N=Z1ilD^u*gc!K zzajtTOmj$!_8%W{w;onj!hUsw%L)fj6XQ?k=P62XaK8UJJeywwI`Xj**WzSr!dmS6%Dr zcP&j)cyeI^s;$mVcB#X_-K0)dX+kM6akOv6%ei-Nr@fI)7VVg#>7rJv7Ucs3lm@Vg zyN}YY`JYD#@-9ph>u#GJ;V6Wl79T|HVQ{4qCXG(z5%WU3vCYzkC&GNgqT>6BtTqI0 zHicpFUyH6z0F3cvOZfB@kBXrZ`4zaJ|4Q73$N?VKAAiXLe`72LDt;c8EDk%cZ?!q# zNZ-OEK@TPcmfbgb+K|$yd8p`p@hVJ7o=P2!1($_Q&#xehLA&q}aEDfcD6P+f#Gn)eOJ+ z8K{6MA;q;7azq&5!Q#?luGA35PPuYLi%J{Iibwm>^vbxkEf&hV#QE!V$DoDtaq#e1 za8^WlpmKX$b-*jb*JYSR&41~Nh8HJg0V5bwOmwh_F=&6flDiK-(OkO(e;N6{r4V3< z7m)2LSMFre-WNC@1jH9#_V26h=z+4pI904Umk%%HZ<3U#Ed4#r&u(o=nYy~92gb*@ z?N(cx26tJig^BBLyYRp`rdB3AxWeH?RH(S?v^%1Wu#C9P@S4pQk@sl2^mdAnxzw(` zBni7+4@1*f`AA5G#iQYPvN6_F1@3(%aQO@BIoyz&)b1Sa%XoltwcUfpS)8hMJV6H3 z&jVsiKKj`yF9?`ps?jbei2mOf5QUid`Mt=#*qv<9vuO?)DgO*VVSwnQG+a8I$%q|p zdcFD^6jh3H+^#YaKn7eETU=gIN*Hqv*B9zR?1t5nn4gD>>zpI(;25$rd+k6(8!LJM z#1Xb)&Zn^5LWHx`T6v_lTAfF|q)I{8=}k0sXII4X4PI$@unIR4%F}CaGt6hJ`zo~l zXtM6gHeje3YnyAGf4?(k>!788&w3+QL5+86akIU@g1Y8wuGAN^msf!U`=1FvqJ*{J z;$RIzNvmWq;QLAs)65#euJOC@Qyai{c&bs_T;HnyewXb1V=IG+zJ*zs2#y~yvJ1@J zFkkCjRxmwwX%FP7yWFM7h81`FhP)u4Nf%@5>00WlI$N{^@kc9v`}i$1R0A@7u?L*$ zIyDl&a7aq5|5+&ii4~N#kEf3~3GVIhhdUTSch!)ED(xHa$8H zb-1kl@|jX+ac^_!0q?~O)Obk9abfe@)B58S4qr$8$V2x#Ane%Q+(Uo@!1Sxb1Nq%Z zX`_!ni1cL5nmnZV3>*E3Dd9NYel1;JU)Ol%?Bi8rn)z)U8K99qj&=W|-+b!fp)G4c z2ex0Ik;{LRHVGrgNEdP0u+=+TNU{@Ha|jl1Ws)s%^)YT#t4t^&+8Kk;zu>aZCDNEC z2{Uv|IH0gL!fZh~3WBeZfL?;Hd2l63l_$y&`>@zf3zZv}iIrcvdx<*f{=_96Q0V#p zmx)xzHw0889p(5;K0b0W>UK47_@5BpT&6JUf*G&@#P_kwhA<;~&-P+H2*VJtn~WUM zOP(Z5nXY)=2iMjzYfSKTzQvY}u*|WRR7PDl@nhbUZNmH>VaQa(8sM0rR;Jwayoadt z7&(VOxm@22np;$BKw<%~Sh^@*=LTIL({69@YqeJE_~Bs!MtSt%^Xt~@!+MuX&VRze zf43!qjS#^0s6sc;-_NClIcwbv-0gVu)kkOy+)j|EvTc5rR3EvqWyoP;V?&9B`dsga z;n!pl0@YXzl2Pu#W#exLDZ1C29+z=r2LNg#(p>Q;_hzaxZ&)-jFrTYoRl7s7MEyVl z$uZ*>_yjXR2#TfmMm2qr2@xpb-oFXL2u}TIes-7lnPU!$9F<-vRg@6!>Y-Z>PF81h z4bR-BkR=pZNNob^ppz#J1v6w!UY^fEeeVM+>I^z`qKnkVm~RH6`E2gAUA*YlV86{A z!eJ~=*{-o?Q!;1XXA|0ag8V7I1;$eW@X{0$&=N86wJ;Gl+de@>Lfb@oh!}BZG%-K! z{k8)Iv%SMNSY1sYlE6@R@q$pQq(x?L46=>$i4Q#vy-oB1au`6D#*R|%9OT|VeLins zoFTfVb4Rm5(O*BpKTqQ}^{u*4PD+jF%GTo zOi8EWJM0H6rH>eQpG>e1m%W~X9eJuS`7Q)ZoPI432@BJA1_2pE0h^xDF)?NGYTP{K z7|6mVB_<~_Xuaf^=<@rq_uB9QXr#hMxCE%P6KJ0;G6mYl%}D#+-ns~hS&u$gz$#iw z)Ci_Uc5&an@Po@DqfA=VnC0kup!N5UL&MYGm2gY%3^I}2k!7tErZ9ICp-1Y66u3L< ztcETY)t6fBA%u`dY{?Ugx{lMinzuKH&G*Kb^KQ9phGP|>SJ0j}(@flhfX>mPIswC^ zY8ozWy{6#ogp#ih-!C{|=yb zTYr5nW)ZIcN#y++E3-67uSeXw8zjb;r?NgvyEW;CkYG?!9dW5O_^@+`)12_N_o4ot z7hwOsz1-pV=aiNGD0ndyCGX#^waQDARm&4 zoDA~)^uZmV?lkF1!pZomB9kXe;%G!@tx3}{aGAh(LWn7QeIivHl*bKi8pEU_R5=T# z>D#40OHx(&NbqPw6bp1p@(6K)gnO|F33B;01^RevUm-;Cy`CLv?fe8?2YRXoCjldB1WL zDW^sojs1&mssu>Te&~Am7}Ly2tB@k~Vor5!80n)>)QxW)K&E|$oXI*0*}Xek0wk=1blliSRLA$vS{;s0aLcY4 zbNUySzR{!;0W3}L&5SmG1*1!Z#Qzb?U25+A(+;9f_|kodXGf=R1ff|#Ns$%*s0BbT z<`UM~B#5rJl30mVDs(Cp<)=?T($T_cD&hNoFCqv)DW|ge3$`vDp03jcmKGi}?g!|c zng7~5w!Nm!MTmmyTA%vu<#jq-g)qg&Cnk1Y1P~CYJ}h%B8j?^K8F^NYPWik!Kut1? z4{l%P{N#*A6BYWyg!uO#Ll?R25Y^y&za2=Gi#j2HD@>+`K0t(>DM5 z<4e7Tb=t?nWgiK6$5Gswq3oFchuW3rwAcmn zWdO_CG~x8wtr_Q9vaIp22s{ZzUm%(U>Gxl18`ck^Mti@Q49Li+W}$yvI^YHar^{iA zyfR&kB25ZSbYv_e)-cwm{fH0OMP#kQ*;=F`)ru6#!*TTcw2_Gbt3jJ64d9ZI(NSw} z;V+z|HVl7CqthnTy?Suxo^4*aHPH!;Q3jmsRBk0ZbEHd@2TjMt>dft6rp3Ml=t2M$TCz^Cr<5H?pt`_2TIT1~lSz1!fGb9)IRen64rEtY8UfT3LQ{8d_ZALCV#IUEX z4aA#mm#31nX#TuH7fTZR>O0!cg=T+KK_ELN&&tg!*BG16%Oo~2zC&jYBCNmf!IGUn zv%U0nCWJ5<1SM2w z1ZjP0)ep;tj624*@u2PvK7u%{__Lh(Q-tV5_N5rWMK|@b>P}sNS;!B+O zxbx0Uh{IaN5@-yV|9SQwxhEe&HAMyfwGoh~0FxX!ya^{wh;^A!7Cly5`4 z{^&a$Fsx}ZS{dIJbdoze()}rNVI z1PV(iwjivsuhDAr(MO1Fgp}>{Um1mY8^~f=C^9DEMvRZ0c}AtEvARVO^CCc>z+I?u zU@Bo6L(9^LQdK|?bn}6p>}Gf1)9AvNyYz5TtU7@zlN@hT-%!xsJe`V!D$KupIeJ`1 zMwR2athsFL;TbZ_JnYiHN(TS^10~JSfqOeOA82BdxT^_SK`1TmcStcr0#v%gfZk=J zDV+ca5-LPAe}F&lUN^P2Rtxx^6b&v@QPj%$LBx^fUAU;_IY>wdl%E7TZ}ZL>zZG2~ zp<}F0q<-+*Jq*?_{6rvL36#MsSk4e5n>Vh`J6i%Y9RL!uuV`5?kwW-X%r)Z#9`-PW zrW`(o0LSi9c);^aTsm+3j{_AF^k~Z`y6C-G>k1y6!I?_%I3G}B`~)OW8$e(Sb{m?T zCBEh4E8_0#^dSHV|3rdS^SQl3Io35plNsunI;kamP8ohB|RfH>mZM;;mdj zfp{F%mpd!-9Lx}i(gl#_P-l!O+IaHT!N!giwm2V zTgk)HyITKg+v zM?2oOA3p-oh!C}Zf!Nhw6Khbdc5)h8Jghxnd-0l3G2Qp2-Be`OTYiuhPxaTgaFG@n0 zrjqVF9YFIvR6sv<@ZrDr>F+D1vSC4FvN#tHPj7i(+HlVNr2fW3oD**U>#xOF(v$K4 z#VQxWhO8g$Z8nv0rl0}$cPK=1^A|p3a(|PI5L9COi&gIV@lPv)T>TT+w>%M#E!Pqs`qdlP8}{Z3D+nS6MoHe4V>RI1(2Au{hviF}Ga5@vc8#x4lkm z*j(5@|1&>KKB9?A9OnAA)D5#xzmJEDX{h8rbLv`@S3_H==$+&$eAJbx1(L0#!V`?`VY|N zg_n~#FJAA*Wv9Z6xX`lI**(&G>i@+zRG4U8<%Dg_a!kwfhzvZK{fy8|WP33>(BBn( zDsy7{*FC54q1qO>A7plRj2CE9e;R}C@PT~M-dXdhG2e`xcVG(5U4X>Om`b|QUTAj@ z&9273S%IRygoT=%+fM5V5!emb2d!?_sx^n*qDg&MY5TqmEu>Al@BAflStv zEiiP$KR7sK7YSF36@W&k&0tTfJ}OCDM!Xb8CWY&C{E*Z~neg8P-tO_P&evd*1YY3slzhZ`%wbrXBL_=Y3s z_*2;=;2s3vN>OKRfDpK@zJ6%Guv~15{rXgUY(xcaXRM@^%q zv28VOY};yV+qTU{jh!^MZA@%5HYaG<_dNf#-Vcy7nYHHpu=l>NYdHjyRLSlYs73!R z3wQ~;1S<+WkHuH+DGwLBjYbg?o|HGzo;^8q_aGX++0>fKB&JFbPAnjQ8wavA7LSee z>FGjqF%0Q_k@P^p56MguP8bDge=KKvP?s|d^*|WTu{as&iLV%?a#;-}cz@yAL%OUH z36`JGPQ}NfAaUioQmt>mF%sn_CGTw-9$Ft$rDtk!5jc*2-t6(q4pU{bT3D7x^E@GM8~{s3ow%fG-0i0!W#;bX}xOB>*^e{0F+Lypx?m$ zG_{<({9VY-tAK|k2)f8bN`BF7gtXyU}VbNJWGwPdLdpt;CuEoI(Tgroyx1f zcwU;8`}egfqas=uDsQJxlp7dSO&Am_5}HC8tF}d;C@+O+m&oI>FlpXHSf41d+aDN; z{?r_iMi)wDeBFYALEAc_57w0<=;8XCVNU(gT7R^(3u-0^F!y@EEaTerg)cHqF&?#L!fSK?$jE{o>_+ zy1bo-?I3}b)gq5;3n-4pSgkx5Q9>Cq=^IBeY1S^$sG@}?geLhEogVUtQHcQyts$Qz z{JmfPB_U*8)P$)}dJ@fj8!r5vNI2g#yfX*ce_{~q%cn^T&mCW6<4%KOgrEupafMe! zN9!w$GHqdL5u>=T$;c^x(0>zAv5SOKItsd_6UTuDVIYT+*6i=M!6sJtuG64#4XEc! z;P-?l7$c7bYjDt~Mx#iNSd^{$R77va#m3&;&#BJ*;w>UaiuS4Z`iBZ^l+T$6Y4bQ= ziG2sdTw+ou(YfXl(h22l8}W;O zaK4zbz0uN^aQv35{zNE@CK1{%rigS+v8eo2eRTnqC62G1Dh{!K2C{maGH7lN!Kc&b zZN$Mp&%0X`TsH%V)3v(WWK4q+>-N!VpEJVDRQPm};6`4)_52&}L<*g}>L=<+mw0gjmukdeZ81-8m*e;-Dv??6&_5!c}p|h92&ZG^g;emyGfJ)$5&W z#BAtVEY`h5x_CbDpeyrC*6qJCvjIL&?<<8V@Af=?mepN3+)G0QuDEx9TA4yIfWoSE~Jur$`;K4xcN=+0M0Y<_gr6g3`FMuxB5_RR<9+ws16Fp}foray( z$R1wfX3b_O?4Ti+ftVnW-xaW# zviFt-?SS8+C|#_h><<709@sbucAv@B*Oq{bXs^eGbu&W>c)N|TwHKhw9#77d2j3mq z0GVa3D}u%+XV46MwzUb_o40)VJtc4U_thBH5`RpRTVMe{&y45iOs&2H%+|EM@14El zJ8y?O&d=R3d|Ru9Zku~g0snTu&&LMCRsw;u1Q?PCI!^jSQuoD%>ev!! zb*NgL+8d)8PEvN?;Va`2z?K~1rL!@dSFZdCylBqtQwHx#2K8NCw&D(zOLRbHL5@na zglD|}JpKOOech6lvLYk|8aV%>ima+Jvdz3J#IVJHVU)k(%H-N#)^)H-cF!G7q|s-w zZ|wp&8PD8NAcuMK^a^Z9ZjZlf&IDo!FC*l2e2 zwbN_kt2e=B$`|3=jt;#NB<&hCn(+Q8^3>0L6=hUWIMNo4cr~0#3?uO6nM_V)^+{5}KRK z!Yz(U7P}-=6o^7Nk~Mgz%hOH78FN+!hJP}NnWSINeb9&b;k)k+bsk7(x%ZZxf|tEr z8JEI_X9*$|AQF^B$Ybo=!Pw0j1N5kmslxhgyWUl0@zuEDMb}8ku-n-)HLQix((G9G zwo4yeK?xl2RPh%n3}qhDpvrE=qxGGgR%KMSb_^lq>wSduNGbd>NFy8+7}4;-M8jEX zByxOOXI<!@Jq0En;G;u z)3euZ<43GxQp{e~S(+eylF)U&Tz>D7y$Q5MG28khlFw=mil0fS;Ybzt1>okfNHP^uHbE{MGA8L`nda>UwZ5mbL|5LhgTkWXGuA z{mT)_5)_8?Jzcst0|xih`tk%WdeILZ{-H*pxYBl`-QtC{Av^Ak!#tQA_p@Jt z#|d!-zph*d*x2XmQ2UTRcf#fT&N(f~3G0%J=d~e2IAvWJ$umD&eI348Df&3`wH;#FcT_D8bnyyxKb3~VmiF$`A7Y^u`?%R{t13suoF89;6hx8^c>6-K zziM@~4LRb>CA<_9CM;kpkfQ+06oy)?4w-g9XavP?fDQTv4aKnmX_%camGz))aiM2gEcDI zAhy-*Fj2(@wgutB^OxeB*Z2w&J!u|V^jkqvUNDVRkXRrFvJ%ttQ!M{=;Jg={5mB+P zk1A%ASAWZb@~4b15jj3(&}p7aK^s+@@Q{6j`!@@>HgH6%vIM{u;|tgf3^|2Mmvznc z!xJTD+TB!J_6bpQEFJHRxGQB_*SlVc6Q-UYH{1{nE62NF&0n1z6OzP!mak0Q$SMOf zS41&oisUdrdgbQkB>?6k13;6822 ziu>{|P@xtLfct^#{bP>J?!QTTs|)&>80+i*MA**ls(ovAq*%qr+k)V`%K)>fon74P69B5C7mmFxU3DjWjT62gFyT zXxrT{@qp1-09dwu$uVhxb@Jq`F>Q4?#g{jW16V`)~s zNimt^Go6}qZTv-FDl1jI4fR|6rDywChe2Y%B;_mM5P)?SJoMlVO-n8zF#(h|?%tlU z1Yg%cp2sYrv-(*Y|CW0zw&je?4~!`POY}Ebd5I(u|hU1>E4i@_g$< zdBW#NSDBTQlk#S4NT6B(h!X&DmG6#)w>N?!mu;`NI9b_+K z=A_T$m)5CJ$wSm3W69P^GN83a^a;86R_ur*zZ{)kG2{qB4D!E`=|&|!H;ot2cV)}Z z71x%ts6~Vqi8gB0I_ZXp2cgOKkZQQF%zX)g_>vLDP>0^eLwBexIwyeb7b!uvBN+XH z^cgbbBXQR-@Vw$f`^OLE&(0O5J)<|jwlFm3jd*2h$ z+lGGqlJZ$!^cCls&?1Mym4(RWuEEZjPe&w=%!>!R? zm15Qvgph+T#EKMNERI0>=@jX=UtM@4*?Ko)Oggrg^DDf?%1`E5%jbDku0$u*rVc_Z zO>D1+i%*){-BS-t4qAn8M}yRS0Urc`2Cs8Zo^JQyap#ICB6Te-61_WRE9Y~oyoKy6 zmD}IW*^&w6r!bcsJbK%QrJ9G{H@=q3Rg@px9hyO*GIJ}J(o^)4?2W&FS>Dz&!9luJ zWwc<*Sz<%}IA^m+G_t&Y-|tR;YmoIHCC#^}f!91GWFVW5FOpqh1TNK9ZoH;j7^nbG zREktDpSVkWAU*jr=XJfOeG;(C@U=gMx&WY%N9{%YZ@M6}%dMEr=QAvzrKgM%9CUtr zu&X)@{#(QG_4A)H&(72Cp~eOOtnn~u_f~glf4N*K=%2R*bc+rmyIKWbns@LeVWXrH zV1KUP7WBFL{`Be*okeuw$JV41;aA~q1Q0L=1vKRN{V5%JDC_5ReV8Jb$13xZxR6`= zic2a<1uxw&vbxGp0O0ze62$t9!w<=_AYfH8a`_WLW4=X+NmFq)>lc3!>0k*170 z886QUKOp3D71yZzFn)KJ%u$(&jo0Jzg`z$A-@F;$ZK+1scz&Xa%HpX#RoG!VW&oQ7 z;G4*dgnfIte&aD(E#t&-{CMMzPdK-=vrAGa0TkFq9vR!S&!B%v z!1*`=10g){+pS(gQyx{eLPcs%QnK?FrCP2j#x1z_pX7gP0u*sNJ0 z>D&L1N(KBe-e|!E1=~R$obVsjA3BjmGBZ(SY&2TnAlSWZ(E+LaAN+exgi8;SR9*I+G@(&O@zevA%TH znhLhp$YQ|2K$pm!bs}%?eiUrgO-D?H$iwuL3{teAh^$76H9#J@n3HA0;*c2iIOJNi z;T?ftS2Ls{tOoBZew=Tp=UD19#Itw3&r|iPSzGD8Z+McRP*bIkXC~ZBoCw*vT7mGR z;O*v1P++IHNaU5J7*LgQ^;M4hsdpw0jI}1)~A3dyfUvQL(^nxZ7 zKZRB9nBoYbzvQ2UfiPW=)ap8P|h!fToCmLH?Sh5?#Wod!}HR zh;T&Lk_W&81uPQ9DpQRvvkTdhQsv~F`WPf{81c3!dn(m(mZyT)2?Atz^SEv6A>}d5 zWVlFVo4-xH&9jR3+jxFcz&;KXmr!Ux3+=Z_8&TA5*drmP9hvt<>|t=1zfd?-q>ICL z$-c@pE(WlI9x9=bhP2y!m8qV(y3%l9L*EQDL%Hv@r$i8Oa$N+~ujt|IY{nSvNj zzuS9!=oDTA_wEmU8l&sS7Pt$1%d7kEo+suQ89vWMP~r_kWF<3$ z@4GaH^wmxqV&L@)QZQuem2KU}hZzslj2{1Jj>DBB@WEn1O{n{VT+PJ@(^;bGG9OhH zUjAK;)vqgomJx%Roy&$0DEczZlVpMq;1e-Z6}?ln_|Ban)HvB|eOUOKbe`MxI3GaC zc*>y6UYC74*qLx~jjf5@4xZ*Ok)eVamHZADF_n|E|Cr9HC?*NP*xRd6wqOcA@?*@# zV}J2>+-Cvi4v!zPIe(|#rhQfLA6blO0Bga0xrj?4%dEu9gPgyI*WLo%DT}?OYj}#) zIz%caY-x6C$Y@PO4sJn9P>(jT$%2*HGqt3oak9WtlEUH8;e(LMvt$T`@F~SO@*uhO&lN9@-nQ{&gWvf2q|H{n>IX@%k29eUT)ZS$DFOc~ z8$RU^edUKlIUXi7fwvoIJwr|0i;0TzZMIa>m6~c7M_RzL%~-O&Pl}oAx{=DW}`@IvIh2`+8=Sn>G|cl z7EJsR5*>@<{NadW_?IN<=tx{x2<6k;dhI*j;4Xw_3<@{d{6v)XRLCw$5``|bOsVdg zzA%d8>m#9@u+Ff{g%w~4v@-fzBO}Av51=DsOUxZVn8VwOi%-vb^2FWSFl2Q3rPg(KU4(RSex6RV zR-@euOodVx;5jvM_Q`&`YkX)GjtyOPbH@{*uLNAh6))PHMSWh@=5~&C+B6!5XtVst zvX6!TBt7pj#q?ameb@5k7Yr@Lzw>|>#n3@g;@aPiHz%lq{`oUSt+Y@_{#*8nW!I8; zmQ{GvU^cejwFdGKME(AkebERwb=&6;QYC6ZKsSu8h>eZFofjC+^JsTHE!^w0>J$?9 z+Iz8ojQvTKMprRJHEt@0K#P@FbzPZPVJPfrtPlKWKt@J=E5(cyNpRhlGn%5N?5TsM zaLs-{lZldEIeX9Ph%}tbE)agml9L-}9B#~!t9awm`RsmR99py%BfAvF8!{`PS03PQ zZ9MEPsN#zLR&=+&Vb zx&3c)%?I|!&)g@!-ltzKE@wSC`>r6M2Xd0z28o~alY|rkN*Jox>ZhFyT@0RqH~0U~ z0tD0g*ZrO5s5h zWHSrCS}CsvyGu0c7bw#`YdzG!L)%=7u2`}peg8Z|Z zFE8#}D8-}=g-}8kr=6oKd@CwhM!n|UB2iUBH@Va8v_d8$BIXKxa}#y)wL17ZB2)|` z`cq{yUh{e`@>T=|8-lu(hj$y}dV369FbYT+?l-;hT7PzWt<%LK+E^kDD=3nPJY1uE z14ptK?X13x++<+Uh6RB<*L{d0vd-aSq^*&=H;O07Nn)?(oZWZpvE~tbS_|%V1_d8C zN-Vg!`FrVc8Qf-M#h{G2QU~O*z5pUw;qhx*ZU-nWn;t@!Cm?ND6%m1d`3>ws%spB1 z#Fzp)&g3soru1k^RxMEa1JHKmVw^TM6t4vjX%)HKxFpGnW>^A@bH!R^>Cy^wFPY#GJ3!`H=|(`_jPJ)vC^SVU09Fh~J|hdCC0FVd@;Vt|pz{>TXdH z*)m+ppP5j2o|GRiM`4$l4U;$H8k48Yqz+xI zQ)R_>T?vH$NUo1Z9hb~1gr8*VB!$|NSs`BibQUKdU9Hx94P7wuJk`w4FDa)EM(L~Q)QhR81w@#ca(wrx7 za;VJzAL{&JI8F^3uN^3i2ihKH=ISIps0&PaI`;n6{Af@rPmEV{`z#T*2 zZ!VM>5E$u2q!|`Z#46EH6Fk=R zKa6qz8YOaA*ZuKQ)dikWwAHJpiPqBMicLi=0{T73$d%=KorC=$Y;WUvzK6Bq9k0TG zV^yj|1!(T&goQK${;yG|a?Xw^5ONAr8n@S7aWy5!_wU2QG1!-H_k8IRMe=3niQ}cX zIAcn5)AgM?W;UH8hm&}*TyD|!E(tnM8f|W}LA*KkeQ}*%xmX3Gm?)>+)B5EAyEoOW zX`8O$k&v$H2%oB;%_v{W8R801_v^#)Y1^p?d^Z{D!VW8NCfl$(d+0EeJZQk(a(G`n zigeY6|A8~%Q#7Y0RD8r}Z)*$0#c}ZP<{vAa!j0XdQ0nUI$FJW#dO?!<+#?nOVBDUM zhgz4tAr?82&I+hnTdw97+NE#I6FL*Stk%I!8#q1y&BO6{h98;emxYN*2vCTE{ruSu zVi3&tA5Up^7NxVZlcRRzpa^IDqojoLYP(CKb%T^mi|mxj@ssC~2o=NH`2#QoCicS; z$~Hsi^GnHvPO5rgr3p6lyYq$V@E5z;V84c{cUaHgUdB7;OimGkzNpW~!E zT4UPPb}LBWZhT04c2vW6j$-k^ZQzRkyrD}wo z=NXTeTuOa8x#Z2+wi*NmlTiuo*urc5*pAFotu@JLuHe}%`xXn#C>2P=#*I-rF%e5Tl%N^~`*msDzSkT*NWY>rx{Ld7T&r1B zc=E%lV+aU(Z@-?rFtA*QjS5BxFfKVraCY zXP2;HZxBW}clW1k3IzPo*H6G}VOZyh0w1dVPLBlr{U?Dij*NGAqArh-OSK#gAu3a4 z{hXORS1|K@euauIo1rj9(Be`nCLz7hn36}A)2y-B>@QU~JIx7{(9uln+=XjPaRI&0 zQq#Liy!A(~@!a$iw=PeRB*=_wy1@!3DGF4UkG^6!mF=IKyn8M;h7Uhk11&7~>|K@2 z>K>6mU@}+&%if^Z1RXOBh2z%S*%^(Na#WGu)ID>jy=ITJ7%|wr1l0q4mn9hlae$PpJwg`B7ULu zk7Lg`9=?gk=knm?t^R%xalR8(PI{k-%-W$M!uwa9H7R^WPi19X#&UD8&O%R6@QDE< z(c~oVJwC9#6osz!_gwk>rE6M25hj3+7igRcs;{^efEcFnqNOHJCao!|HL7$zw{X&J z){rO=5qY2}tY693qM5bggwJ8Da79?TW!PfgFG&=cwQG?+ZoK!+6MW$`_mzT$g>~YO z$gwLKSh;*ud${z)T2XKJ%g>A?Gv3XPc;-uDOT~28Ok* zqXnW7p6AuAEYF`Dx^Ce&7@4~7`JzHQKAUCWhv;g5ssmEh+TQ?8)}3JS#`$gU`}MzM zrhi2NDEpx&Nd(d#Ckg*j`}>8-dI$Royy%*FLZXm(c_-=#@fgTpe9u(#8sdd2)Qk-c z`{p;VHkA>ALP{8N6#6D6V8Jf?0GTvZ6zn2Jo0=b%Ze+91QgtqaQoI1q)xiv^?)(br z$e*B5CC!+32y_h@n3zsEJZ|1=auaGU$tkM{cu4?$m5G#{C=Cwk z^^AlCDBTsx8^1UEbcJpO2b&nC>6tstZ2XoSOv|}E`UB^SqR)SK^Lj^>+T|o9mt5Kj zaul*Ps(`T&93zx+mIB1AkKayb9l?O|tPyF3=R#oJpm&;pLuT#sIvnYVt6Grg38+_9 zL4q~2DfKs?Ot^anx~hUkj*f}$opzMJ!ch3U?}5E@`u?6B9Uc9ZK!}{;#`2K#IN%N$ zdKhZlG&uh_MT$!580-l>Xm;*4%#PS>h$pJJl`oO?P)Hk!Z;}UlE0G;HPjNjQmv1Yh zsx^bN?MKv>$}`GLc0;~Y@1LL#8gij|HqAR!98y672l=f6os&8~wr{P$UI zz9^~tl2}OD6t4l~*&@~Cgz^$6<)V1@%$qYK;KgCqRe5a3kS+2PzZto`Y@Re zrQ-N^rCKfdH>!R`nHYAREtqppv0E*PR?>mo9a~E}YpTTcOw`aoi!ZE)%(Ti;I@mSN z_ZRT7s!(*w>S%XYU$-qz$z@%0AcmuV>hcGqsWRn~oK{XhtyYL@Zy^_jm_4q`Gj?NB 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

XOL$)4m5(Xu+S5D&NtuY?yY;g^yaJl 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

&m@ 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Ȃp1yO%>ziAK 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

I{*T&oMy-UOu?@XTL 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 z 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�UB8A`W|p-jB{fNAJqjKN*%5MNbw&L)k+ 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

V$%w&r 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

KPS+y@^E^!#Ibf7{ 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 U6syxW82|tP07*qoM6N<$f@JHT!vFvP From 03d05cf0aaf4b321254721b652eaecbcb95ea09b Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 25 Aug 2025 19:47:47 -0700 Subject: [PATCH 099/102] implementing support for asynchronous texture loading and improved Vulkan queue handling. Bistro.gltf loads all textures, and creates physics geometry in under 44 seconds. Seeing 55 fps as well. --- attachments/simple_engine/engine.cpp | 5 + attachments/simple_engine/imgui_system.cpp | 62 +++ attachments/simple_engine/main.cpp | 10 +- attachments/simple_engine/memory_pool.cpp | 131 ++++- attachments/simple_engine/memory_pool.h | 4 + attachments/simple_engine/model_loader.cpp | 265 ++++----- attachments/simple_engine/physics_system.cpp | 40 +- attachments/simple_engine/physics_system.h | 22 + attachments/simple_engine/renderer.h | 119 +++- attachments/simple_engine/renderer_core.cpp | 50 +- .../simple_engine/renderer_rendering.cpp | 29 +- .../simple_engine/renderer_resources.cpp | 509 ++++++++++++------ attachments/simple_engine/renderer_utils.cpp | 28 +- attachments/simple_engine/scene_loading.cpp | 24 +- 14 files changed, 920 insertions(+), 378 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index a9349e6f..06113228 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -285,6 +285,11 @@ void Engine::handleMouseInput(float x, float y, uint32_t buttons) { // Check if ImGui wants to capture mouse input first bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + // Suppress right-click while loading + if (renderer && renderer->IsLoading()) { + buttons &= ~2u; // clear right button bit + } + if (!imguiWantsMouse) { // Handle mouse click for ball throwing (right mouse button) if (buttons & 2) { // Right mouse button (bit 1) diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index ecab5304..e3d17095 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -123,6 +123,55 @@ void ImGuiSystem::NewFrame() { ImGui::NewFrame(); + // Loading overlay: show only a fullscreen progress bar while model/textures are loading + if (renderer) { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + const bool modelLoading = renderer->IsLoading(); + if (modelLoading || (scheduled > 0 && completed < scheduled)) { + ImGuiIO& io = ImGui::GetIO(); + // Suppress right-click while loading + if (io.MouseDown[1]) io.MouseDown[1] = false; + + const ImVec2 dispSize = io.DisplaySize; + + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(dispSize); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNav; + + if (ImGui::Begin("##LoadingOverlay", nullptr, flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + // Center the progress elements + const float barWidth = dispSize.x * 0.8f; + const float barX = (dispSize.x - barWidth) * 0.5f; + const float barY = dispSize.y * 0.45f; + ImGui::SetCursorPos(ImVec2(barX, barY)); + ImGui::BeginGroup(); + float frac = (scheduled > 0) ? (float)completed / (float)scheduled : 0.0f; + ImGui::ProgressBar(frac, ImVec2(barWidth, 0.0f)); + ImGui::Dummy(ImVec2(0.0f, 10.0f)); + ImGui::SetCursorPosX(barX); + if (modelLoading) { + ImGui::Text("Loading model..."); + } else { + ImGui::Text("Loading textures: %u / %u", completed, scheduled); + } + ImGui::EndGroup(); + ImGui::PopStyleVar(); + } + ImGui::End(); + // Do not draw the rest of the UI until loading finishes + return; + } + } + // Create HRTF Audio Control UI ImGui::Begin("HRTF Audio Controls"); ImGui::Text("Hello, Vulkan!"); @@ -359,6 +408,19 @@ void ImGuiSystem::NewFrame() { ImGui::Text("Camera: Manual control (WASD + mouse)"); } + // Texture loading progress + if (renderer) { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + if (scheduled > 0 && completed < scheduled) { + ImGui::Separator(); + float frac = scheduled ? (float)completed / (float)scheduled : 1.0f; + ImGui::Text("Loading textures: %u / %u", completed, scheduled); + ImGui::ProgressBar(frac, ImVec2(-FLT_MIN, 0.0f)); + ImGui::Text("You can continue interacting while textures stream in..."); + } + } + ImGui::End(); } diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index 5afeba11..ba6b0ddb 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -5,6 +5,7 @@ #include #include +#include // Constants constexpr int WINDOW_WIDTH = 800; @@ -38,8 +39,13 @@ void SetupScene(Engine* engine) { // Set the camera as the active camera engine->SetActiveCamera(camera); - // Load GLTF model synchronously on the main thread - LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); + // Kick off GLTF model loading on a background thread so the main loop can start and render the UI/progress bar + if (auto* renderer = engine->GetRenderer()) { + renderer->SetLoading(true); + } + std::thread([engine]{ + LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); + }).detach(); } #if defined(PLATFORM_ANDROID) diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp index a150592f..2e2fbae1 100644 --- a/attachments/simple_engine/memory_pool.cpp +++ b/attachments/simple_engine/memory_pool.cpp @@ -1,11 +1,13 @@ #include "memory_pool.h" #include #include +#include MemoryPool::MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalDevice& physicalDevice) : device(device), physicalDevice(physicalDevice) { } + MemoryPool::~MemoryPool() { // RAII will handle cleanup automatically std::lock_guard lock(poolMutex); @@ -153,6 +155,48 @@ std::unique_ptr MemoryPool::createMemoryBlock(PoolType return block; } +std::unique_ptr MemoryPool::createMemoryBlockWithType(PoolType poolType, vk::DeviceSize size, uint32_t memoryTypeIndex) { + auto configIt = poolConfigs.find(poolType); + if (configIt == poolConfigs.end()) { + throw std::runtime_error("Pool type not configured"); + } + const PoolConfig& config = configIt->second; + + // Allocate the memory block with the exact requested size + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = size, + .memoryTypeIndex = memoryTypeIndex + }; + + // Determine properties from the chosen memory type + const auto memProps = physicalDevice.getMemoryProperties(); + if (memoryTypeIndex >= memProps.memoryTypeCount) { + throw std::runtime_error("Invalid memoryTypeIndex for createMemoryBlockWithType"); + } + const vk::MemoryPropertyFlags typeProps = memProps.memoryTypes[memoryTypeIndex].propertyFlags; + + auto block = std::unique_ptr(new MemoryBlock{ + .memory = vk::raii::DeviceMemory(device, allocInfo), + .size = size, + .used = 0, + .memoryTypeIndex = memoryTypeIndex, + .isMapped = false, + .mappedPtr = nullptr, + .freeList = {}, + .allocationUnit = config.allocationUnit + }); + + block->isMapped = (typeProps & vk::MemoryPropertyFlagBits::eHostVisible) != vk::MemoryPropertyFlags{}; + if (block->isMapped) { + block->mappedPtr = block->memory.mapMemory(0, size); + } + + const size_t numUnits = static_cast(block->size / config.allocationUnit); + block->freeList.resize(numUnits, true); + + return block; +} + std::pair MemoryPool::findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { auto poolIt = pools.find(poolType); if (poolIt == pools.end()) { @@ -162,27 +206,42 @@ std::pair MemoryPool::findSuitableBlock(PoolTy auto& poolBlocks = poolIt->second; const PoolConfig& config = poolConfigs[poolType]; - // Calculate required units (accounting for alignment) + // Calculate required units (accounting for size alignment) const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; - const size_t requiredUnits = (alignedSize + config.allocationUnit - 1) / config.allocationUnit; + const size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); - // Search existing blocks for sufficient free space + // Search existing blocks for sufficient free space with proper offset alignment for (const auto& block : poolBlocks) { - // Find consecutive free units - size_t consecutiveFree = 0; - size_t startUnitCandidate = 0; - for (size_t i = 0; i < block->freeList.size(); ++i) { - if (block->freeList[i]) { - if (consecutiveFree == 0) { - startUnitCandidate = i; - } - consecutiveFree++; - if (consecutiveFree >= requiredUnits) { - return {block.get(), startUnitCandidate}; - } - } else { - consecutiveFree = 0; + const vk::DeviceSize unit = config.allocationUnit; + const size_t totalUnits = block->freeList.size(); + + size_t i = 0; + while (i < totalUnits) { + // Ensure starting unit produces an offset aligned to 'alignment' + vk::DeviceSize startOffset = static_cast(i) * unit; + if ((alignment > 0) && (startOffset % alignment != 0)) { + // Advance i to the next unit that aligns with 'alignment' + const vk::DeviceSize remainder = startOffset % alignment; + const vk::DeviceSize advanceBytes = alignment - remainder; + const size_t advanceUnits = static_cast((advanceBytes + unit - 1) / unit); + i += std::max(advanceUnits, 1); + continue; } + + // From aligned i, check for consecutive free units + size_t consecutiveFree = 0; + size_t j = i; + while (j < totalUnits && block->freeList[j] && consecutiveFree < requiredUnits) { + ++consecutiveFree; + ++j; + } + + if (consecutiveFree >= requiredUnits) { + return {block.get(), i}; + } + + // Move past the checked range + i = (j > i) ? j : (i + 1); } } @@ -333,13 +392,41 @@ std::pair> MemoryPool:: vk::raii::Image image(device, imageInfo); - // Get memory requirements + // Get memory requirements for this image vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); - // Allocate from texture pool - auto allocation = allocate(PoolType::TEXTURE_IMAGE, memRequirements.size, memRequirements.alignment); - if (!allocation) { - throw std::runtime_error("Failed to allocate memory from texture pool"); + // Pick a memory type compatible with this image + uint32_t memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + // Create a dedicated memory block for this image with the exact type and size + std::unique_ptr allocation; + { + std::lock_guard lock(poolMutex); + auto poolIt = pools.find(PoolType::TEXTURE_IMAGE); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace(PoolType::TEXTURE_IMAGE).first; + } + auto& poolBlocks = poolIt->second; + auto block = createMemoryBlockWithType(PoolType::TEXTURE_IMAGE, memRequirements.size, memoryTypeIndex); + + // Prepare allocation that uses the new block from offset 0 + allocation = std::make_unique(); + allocation->memory = *block->memory; + allocation->offset = 0; + allocation->size = memRequirements.size; + allocation->memoryTypeIndex = memoryTypeIndex; + allocation->isMapped = block->isMapped; + allocation->mappedPtr = block->mappedPtr; + + // Mark the entire block as used + block->used = memRequirements.size; + const size_t units = block->freeList.size(); + for (size_t i = 0; i < units; ++i) { + block->freeList[i] = false; + } + + // Keep the block owned by the pool for lifetime management and deallocation support + poolBlocks.push_back(std::move(block)); } // Bind memory to image diff --git a/attachments/simple_engine/memory_pool.h b/attachments/simple_engine/memory_pool.h index 49dc7641..c4deeec2 100644 --- a/attachments/simple_engine/memory_pool.h +++ b/attachments/simple_engine/memory_pool.h @@ -57,6 +57,8 @@ class MemoryPool { private: const vk::raii::Device& device; const vk::raii::PhysicalDevice& physicalDevice; + vk::PhysicalDeviceMemoryProperties memPropsCache{}; + // Pool configurations struct PoolConfig { @@ -78,6 +80,8 @@ class MemoryPool { // Helper methods uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; std::unique_ptr createMemoryBlock(PoolType poolType, vk::DeviceSize size); + // Create a memory block with an explicit memory type index (used for images requiring a specific type) + std::unique_ptr createMemoryBlockWithType(PoolType poolType, vk::DeviceSize size, uint32_t memoryTypeIndex); std::pair findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment); public: diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index ffef49f8..90b09076 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -302,15 +302,12 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { const auto& image = gltfModel.images[imageIndex]; std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - material->albedoTexturePath = textureId; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->albedoTexturePath = textureId; } else if (!image.uri.empty()) { - std::vector data; int w=0,h=0,c=0; std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - material->albedoTexturePath = textureId; - } + renderer->LoadTextureAsync(filePath); + material->albedoTexturePath = filePath; } } } @@ -327,14 +324,14 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = "gltf_specGloss_" + std::to_string(texIndex); const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - material->specGlossTexturePath = textureId; - material->metallicRoughnessTexturePath = textureId; // reuse binding 2 - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->specGlossTexturePath = textureId; + material->metallicRoughnessTexturePath = textureId; // reuse binding 2 } else if (!image.uri.empty()) { std::vector data; int w=0,h=0,c=0; std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + if (LoadKTX2FileToRGBA(filePath, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(textureId, data.data(), w, h, c); material->specGlossTexturePath = textureId; material->metallicRoughnessTexturePath = textureId; // reuse binding 2 } @@ -374,25 +371,16 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cout << " Image data size: " << image.image.size() << ", URI: " << image.uri << std::endl; if (!image.image.empty()) { // Always use memory-based upload (KTX2 already decoded by SetImageLoader) - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - material->albedoTexturePath = textureId; - std::cout << " Loaded base color texture from memory: " << textureId << std::endl; - } else { - std::cerr << " Failed to load base color texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->albedoTexturePath = textureId; + std::cout << " Scheduled base color texture upload from memory: " << textureId << std::endl; } else if (!image.uri.empty()) { - // Fallback: load a KTX2 file directly and upload from memory - std::vector data; - int w=0,h=0,c=0; + // Offload KTX2 file reading/upload to renderer thread pool std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - material->albedoTexturePath = textureId; - std::cout << " Loaded base color KTX2 file: " << filePath << std::endl; - } else { - std::cerr << " Failed to load base color KTX2 file: " << filePath << std::endl; - } + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->albedoTexturePath = textureId; + std::cout << " Scheduled base color KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { std::cerr << " Warning: No decoded image bytes for base color texture index " << texIndex << std::endl; } @@ -411,23 +399,16 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - // Load embedded texture data - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Successfully loaded embedded metallic-roughness texture: " << textureId << std::endl; - } else { - std::cerr << " Failed to load embedded metallic-roughness texture: " << textureId << std::endl; - } + // Load embedded texture data asynchronously + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded metallic-roughness texture upload: " << textureId << std::endl; } else if (!image.uri.empty()) { - // Fallback: load KTX2 from a file and upload to memory - std::vector data; int w=0,h=0,c=0; + // Offload KTX2 file reading/upload to renderer thread pool std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - std::cout << " Loaded metallic-roughness KTX2 file: " << filePath << std::endl; - } else { - std::cerr << " Failed to load metallic-roughness KTX2 file: " << filePath << std::endl; - } + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->metallicRoughnessTexturePath = textureId; + std::cout << " Scheduled metallic-roughness KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; } @@ -461,25 +442,17 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[imageIndex]; if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - material->normalTexturePath = textureId; - std::cout << " Loaded normal texture from memory: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load normal texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->normalTexturePath = textureId; + std::cout << " Scheduled normal texture upload from memory: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { - // Fallback: load KTX2 from a file and upload to memory - std::vector data; int w=0,h=0,c=0; + // Offload KTX2 file reading/upload to renderer thread pool std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - material->normalTexturePath = textureId; - std::cout << " Loaded normal KTX2 file: " << filePath << std::endl; - } else { - std::cerr << " Failed to load normal KTX2 file: " << filePath << std::endl; - } + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->normalTexturePath = textureId; + std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; } @@ -498,25 +471,17 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - // Load embedded texture data - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Successfully loaded embedded occlusion texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load embedded occlusion texture: " << textureId << std::endl; - } + // Schedule embedded texture upload + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded occlusion texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { - // Fallback: load KTX2 from a file and upload to memory - std::vector data; int w=0,h=0,c=0; + // Offload KTX2 file reading/upload to renderer thread pool std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - material->occlusionTexturePath = textureId; - std::cout << " Loaded occlusion KTX2 file: " << filePath << std::endl; - } else { - std::cerr << " Failed to load occlusion KTX2 file: " << filePath << std::endl; - } + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->occlusionTexturePath = textureId; + std::cout << " Scheduled occlusion KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; } @@ -535,25 +500,17 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - // Load embedded texture data - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Successfully loaded embedded emissive texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load embedded emissive texture: " << textureId << std::endl; - } + // Schedule embedded texture upload + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded emissive texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { - // Fallback: load KTX2 from a file and upload to memory - std::vector data; int w=0,h=0,c=0; + // Offload KTX2 file reading/upload to renderer thread pool std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - material->emissiveTexturePath = textureId; - std::cout << " Loaded emissive KTX2 file: " << filePath << std::endl; - } else { - std::cerr << " Failed to load emissive KTX2 file: " << filePath << std::endl; - } + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->emissiveTexturePath = textureId; + std::cout << " Scheduled emissive KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { std::cerr << " Warning: No decoded bytes for emissive texture index " << texIndex << std::endl; } @@ -602,18 +559,18 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { texIdOrPath = baseTexturePath + image.uri; // Try loading from a KTX2 file on disk first std::vector data; int w=0,h=0,c=0; - if (LoadKTX2FileToRGBA(texIdOrPath, data, w, h, c) && renderer->LoadTextureFromMemory(texIdOrPath, data.data(), w, h, c)) { + if (LoadKTX2FileToRGBA(texIdOrPath, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(texIdOrPath, data.data(), w, h, c); mat->albedoTexturePath = texIdOrPath; - std::cout << " Loaded base color KTX2 file (KHR_specGloss): " << texIdOrPath << std::endl; + std::cout << " Scheduled base color KTX2 file upload (KHR_specGloss): " << texIdOrPath << std::endl; } } if (mat->albedoTexturePath.empty() && !image.image.empty()) { // Upload embedded image data (already decoded via our image loader when KTX2) texIdOrPath = "gltf_baseColor_" + std::to_string(texIndex); - if (renderer->LoadTextureFromMemory(texIdOrPath, image.image.data(), image.width, image.height, image.component)) { + renderer->LoadTextureFromMemoryAsync(texIdOrPath, image.image.data(), image.width, image.height, image.component); mat->albedoTexturePath = texIdOrPath; - std::cout << " Loaded base color texture from memory (KHR_specGloss): " << texIdOrPath << std::endl; - } + std::cout << " Scheduled base color texture upload from memory (KHR_specGloss): " << texIdOrPath << std::endl; } } } @@ -653,11 +610,10 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load KTX2 (or KTX) file via libktx then upload from memory std::vector data; int w=0,h=0,c=0; if (LoadKTX2FileToRGBA(cand, data, w, h, c)) { - if (renderer->LoadTextureFromMemory(cand, data.data(), w, h, c)) { + renderer->LoadTextureFromMemoryAsync(cand, data.data(), w, h, c); mat->albedoTexturePath = cand; - std::cout << " Derived base color from normal sibling: " << cand << std::endl; + std::cout << " Scheduled derived base color upload from normal sibling: " << cand << std::endl; break; - } } } } @@ -694,20 +650,16 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string textureId = baseTexturePath + imageUri; // use path string as ID for cache if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - mat->albedoTexturePath = textureId; - std::cout << " Loaded base color texture from memory (by name): " << textureId << std::endl; - break; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + mat->albedoTexturePath = textureId; + std::cout << " Scheduled base color upload from memory (by name): " << textureId << std::endl; + break; } else { - // Fallback: load KTX2 file from disk - std::vector data; int w=0,h=0,c=0; - if (LoadKTX2FileToRGBA(textureId, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { - mat->albedoTexturePath = textureId; - std::cout << " Loaded base color KTX2 file (by name): " << textureId << std::endl; - break; - } + // Fallback: offload KTX2 file load to renderer threads + renderer->LoadTextureAsync(textureId); + mat->albedoTexturePath = textureId; + std::cout << " Scheduled base color KTX2 load from file (by name): " << textureId << std::endl; + break; } } } @@ -1090,14 +1042,10 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { const auto& image = gltfModel.images[imageIndex]; if (!image.image.empty()) { if (!loadedTextures.contains(textureId)) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - loadedTextures.insert(textureId); - std::cout << " Loaded baseColor texture from memory: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load baseColor texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + loadedTextures.insert(textureId); + std::cout << " Scheduled baseColor texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else { std::cout << " Using cached baseColor texture: " << textureId << std::endl; } @@ -1131,13 +1079,10 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Use the relative path from the GLTF directory std::string textureId = baseTexturePath + imageUri; if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - materialMesh.baseColorTexturePath = textureId; - materialMesh.texturePath = textureId; - std::cout << " Loaded baseColor texture from memory (heuristic): " << textureId << std::endl; - } else { - std::cerr << " Failed to load heuristic baseColor texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; + std::cout << " Scheduled baseColor upload from memory (heuristic): " << textureId << std::endl; } else { // Fallback: load KTX2 from the file path and upload into memory std::vector data; int w=0,h=0,c=0; @@ -1169,23 +1114,19 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { // Load embedded texture data - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Loaded embedded normal texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load embedded normal texture: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded normal texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { // Fallback: load KTX2 from a file and upload to memory std::vector data; int w=0,h=0,c=0; std::string filePath = baseTexturePath + image.uri; - if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && - renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + if (LoadKTX2FileToRGBA(filePath, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(textureId, data.data(), w, h, c); materialMesh.normalTexturePath = textureId; - std::cout << " Loaded normal KTX2 file: " << filePath << std::endl; + std::cout << " Scheduled normal KTX2 upload: " << filePath << std::endl; } else { - std::cerr << " Failed to load normal KTX2 file: " << filePath << std::endl; + std::cerr << " Failed to decode normal KTX2 file: " << filePath << std::endl; } } else { std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; @@ -1203,12 +1144,9 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { std::string textureId = baseTexturePath + imageUri; if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - materialMesh.normalTexturePath = textureId; - std::cout << " Loaded normal texture from memory (heuristic): " << textureId << std::endl; - } else { - std::cerr << " Failed to load heuristic normal texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.normalTexturePath = textureId; + std::cout << " Scheduled normal upload from memory (heuristic): " << textureId << std::endl; } else { std::cerr << " Warning: Heuristic normal image has no decoded bytes: " << imageUri << std::endl; } @@ -1230,14 +1168,10 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - materialMesh.metallicRoughnessTexturePath = textureId; - std::cout << " Loaded metallic-roughness texture from memory: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load metallic-roughness texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.metallicRoughnessTexturePath = textureId; + std::cout << " Scheduled metallic-roughness texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else { std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; } @@ -1300,12 +1234,9 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { std::string textureId = baseTexturePath + imageUri; if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - materialMesh.occlusionTexturePath = textureId; - std::cout << " Loaded occlusion texture from memory (heuristic): " << textureId << std::endl; - } else { - std::cerr << " Failed to load heuristic occlusion texture from memory: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.occlusionTexturePath = textureId; + std::cout << " Scheduled occlusion upload from memory (heuristic): " << textureId << std::endl; } else { std::cerr << " Warning: Heuristic occlusion image has no decoded bytes: " << imageUri << std::endl; } @@ -1328,13 +1259,9 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { // Load embedded texture data - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Loaded embedded emissive texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load embedded emissive texture: " << textureId << std::endl; - } + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded emissive texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { // Record external texture file path (loaded later by renderer) std::string texturePath = baseTexturePath + image.uri; diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index fc2adf7b..fd3481b3 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -202,6 +202,25 @@ bool PhysicsSystem::Initialize() { } void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) { + // Drain any pending rigid body creations queued from background threads + std::vector toCreate; + { + std::lock_guard lk(pendingMutex); + if (!pendingCreations.empty()) { + toCreate.swap(pendingCreations); + } + } + for (const auto& pc : toCreate) { + if (!pc.entity) continue; + if (rigidBodies.size() >= maxGPUObjects) break; // avoid oversubscription + RigidBody* rb = CreateRigidBody(pc.entity, pc.shape, pc.mass); + if (rb) { + rb->SetKinematic(pc.kinematic); + rb->SetRestitution(pc.restitution); + rb->SetFriction(pc.friction); + } + } + // GPU-ONLY physics - NO CPU fallback available // Check if GPU physics is properly initialized and available @@ -226,6 +245,17 @@ void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) { CleanupMarkedBodies(); } +void PhysicsSystem::EnqueueRigidBodyCreation(Entity* entity, + CollisionShape shape, + float mass, + bool kinematic, + float restitution, + float friction) { + if (!entity) return; + std::lock_guard lk(pendingMutex); + pendingCreations.push_back(PendingCreation{entity, shape, mass, kinematic, restitution, friction}); +} + RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, float mass) { // Create a new rigid body auto rigidBody = std::make_unique(entity, shape, mass); @@ -995,8 +1025,10 @@ void PhysicsSystem::UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) co if (!rigidBodies.empty()) { // Use persistent mapped memory for physics buffer auto* gpuData = static_cast(vulkanResources.persistentPhysicsMemory); - for (size_t i = 0; i < rigidBodies.size(); i++) { + const size_t count = std::min(rigidBodies.size(), static_cast(maxGPUObjects)); + for (size_t i = 0; i < count; i++) { const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); + if (!concreteRigidBody) { continue; } gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, @@ -1083,7 +1115,7 @@ void PhysicsSystem::UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) co // Update params buffer PhysicsParams params{}; params.deltaTime = deltaTime.count() * 0.001f; // Use actual deltaTime instead of fixed timestep - params.numBodies = static_cast(rigidBodies.size()); + params.numBodies = static_cast(std::min(rigidBodies.size(), static_cast(maxGPUObjects))); params.maxCollisions = maxGPUCollisions; params.padding = 0.0f; // Initialize padding to zero for proper std140 alignment params.gravity = glm::vec4(gravity, 0.0f); // Pack gravity into vec4 with padding @@ -1172,8 +1204,10 @@ void PhysicsSystem::ReadbackGPUPhysicsData() const { if (!rigidBodies.empty()) { // Use persistent mapped memory for physics buffer readback const auto* gpuData = static_cast(vulkanResources.persistentPhysicsMemory); - for (size_t i = 0; i < rigidBodies.size(); i++) { + const size_t count = std::min(rigidBodies.size(), static_cast(maxGPUObjects)); + for (size_t i = 0; i < count; i++) { const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); + if (!concreteRigidBody) { continue; } // Skip kinematic bodies if (concreteRigidBody->IsKinematic()) { diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index ed4c1d3b..96e5bb0a 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -5,6 +5,7 @@ #include #include #include +#include class Entity; class Renderer; @@ -287,11 +288,32 @@ class PhysicsSystem { */ void SetCameraPosition(const glm::vec3& _cameraPosition) { cameraPosition = _cameraPosition; } + // Thread-safe enqueue for rigid body creation from any thread + void EnqueueRigidBodyCreation(Entity* entity, + CollisionShape shape, + float mass, + bool kinematic, + float restitution, + float friction); + private: /** * @brief Clean up rigid bodies that are marked for removal. */ void CleanupMarkedBodies(); + + // Pending rigid body creations queued from background threads + struct PendingCreation { + Entity* entity; + CollisionShape shape; + float mass; + bool kinematic; + float restitution; + float friction; + }; + std::mutex pendingMutex; + std::vector pendingCreations; + // Rigid bodies std::vector> rigidBodies; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 14077b38..78c52f9e 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -8,8 +8,13 @@ #include #include #include +#include #include #include +#include +#include +#include +#include #include "platform.h" #include "entity.h" @@ -17,6 +22,7 @@ #include "camera_component.h" #include "memory_pool.h" #include "model_loader.h" +#include "thread_pool.h" // Forward declarations class ImGuiSystem; @@ -28,6 +34,7 @@ struct QueueFamilyIndices { std::optional graphicsFamily; std::optional presentFamily; std::optional computeFamily; + std::optional transferFamily; // optional dedicated transfer queue family [[nodiscard]] bool isComplete() const { return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); @@ -194,6 +201,10 @@ class Renderer { */ const vk::raii::Device& GetRaiiDevice() const { return device; } + // Expose uploads timeline semaphore and last value for external waits + vk::Semaphore GetUploadsTimelineSemaphore() const { return *uploadsTimeline; } + uint64_t GetUploadsTimelineValue() const { return uploadTimelineValue.load(std::memory_order_relaxed); } + /** * @brief Get the compute queue. * @return The compute queue. @@ -218,7 +229,11 @@ class Renderer { * @return The compute queue family index. */ uint32_t GetComputeQueueFamilyIndex() const { - return queueFamilyIndices.computeFamily.value(); + if (queueFamilyIndices.computeFamily.has_value()) { + return queueFamilyIndices.computeFamily.value(); + } + // Fallback to graphics family to avoid crashes on devices without a separate compute queue + return queueFamilyIndices.graphicsFamily.value(); } /** @@ -227,14 +242,17 @@ class Renderer { * @param fence The fence to signal when the operation completes. */ void SubmitToComputeQueue(vk::CommandBuffer commandBuffer, vk::Fence fence) const { - // Use mutex to ensure thread-safe access to compute queue - { - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &commandBuffer - }; - std::lock_guard lock(queueMutex); + // Use mutex to ensure thread-safe access to queues + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer + }; + std::lock_guard lock(queueMutex); + // Prefer compute queue when available; otherwise, fall back to graphics queue to avoid crashes + if (*computeQueue) { computeQueue.submit(submitInfo, fence); + } else { + graphicsQueue.submit(submitInfo, fence); } } @@ -264,6 +282,9 @@ class Renderer { */ bool LoadTexture(const std::string& texturePath); + // Asynchronous texture loading APIs (thread-pool backed) + std::future LoadTextureAsync(const std::string& texturePath); + /** * @brief Load a texture from raw image data in memory. * @param textureId The identifier for the texture. @@ -276,6 +297,48 @@ class Renderer { bool LoadTextureFromMemory(const std::string& textureId, const unsigned char* imageData, int width, int height, int channels); + // Asynchronous upload from memory (RGBA/RGB/other). Safe for concurrent calls. + std::future LoadTextureFromMemoryAsync(const std::string& textureId, const unsigned char* imageData, + int width, int height, int channels); + + // Progress query for UI + uint32_t GetTextureTasksScheduled() const { return textureTasksScheduled.load(); } + uint32_t GetTextureTasksCompleted() const { return textureTasksCompleted.load(); } + + // Global loading state (model/scene) + bool IsLoading() const { return loadingFlag.load(); } + void SetLoading(bool v) { loadingFlag.store(v); } + + // Texture aliasing: map canonical IDs to actual loaded keys (e.g., file paths) to avoid duplicates + inline void RegisterTextureAlias(const std::string& aliasId, const std::string& targetId) { + std::unique_lock lock(textureResourcesMutex); + if (aliasId.empty() || targetId.empty()) return; + // Resolve targetId without re-locking by walking the alias map directly + std::string resolved = targetId; + for (int i = 0; i < 8; ++i) { + auto it = textureAliases.find(resolved); + if (it == textureAliases.end()) break; + if (it->second == resolved) break; + resolved = it->second; + } + if (aliasId == resolved) { + textureAliases.erase(aliasId); + } else { + textureAliases[aliasId] = resolved; + } + } + inline std::string ResolveTextureId(const std::string& id) const { + std::shared_lock lock(textureResourcesMutex); + std::string cur = id; + for (int i = 0; i < 8; ++i) { // prevent pathological cycles + auto it = textureAliases.find(cur); + if (it == textureAliases.end()) break; + if (it->second == cur) break; // self-alias guard + cur = it->second; + } + return cur; + } + /** * @brief Transition an image layout. * @param image The image. @@ -387,6 +450,13 @@ class Renderer { */ void updateAllDescriptorSetsWithNewLightBuffers(); + // Upload helper: record both layout transitions and the copy in a single submit with a fence + void uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + const std::vector& regions, + uint32_t mipLevels = 1); + vk::Format findDepthFormat(); /** @@ -487,12 +557,21 @@ class Renderer { // Command pool and buffers vk::raii::CommandPool commandPool = nullptr; std::vector commandBuffers; + // Protect usage of shared commandPool for transient command buffers + mutable std::mutex commandMutex; + + // Dedicated transfer queue (falls back to graphics if unavailable) + vk::raii::Queue transferQueue = nullptr; // Synchronization objects std::vector imageAvailableSemaphores; std::vector renderFinishedSemaphores; std::vector inFlightFences; + // Upload timeline semaphore for transfer -> graphics handoff (signaled per upload) + vk::raii::Semaphore uploadsTimeline = nullptr; + std::atomic uploadTimelineValue{0}; + // Depth buffer vk::raii::Image depthImage = nullptr; std::unique_ptr depthImageAllocation = nullptr; @@ -522,6 +601,27 @@ class Renderer { uint32_t mipLevels = 1; // Store number of mipmap levels }; std::unordered_map textureResources; + // Protect concurrent access to textureResources + mutable std::shared_mutex textureResourcesMutex; + + // Texture aliasing: maps alias (canonical) IDs to actual loaded keys + std::unordered_map textureAliases; + + // Per-texture load de-duplication (serialize loads of the same texture ID only) + mutable std::mutex textureLoadStateMutex; + std::condition_variable textureLoadStateCv; + std::unordered_set texturesLoading; + + // Serialize GPU-side texture upload (image/buffer creation, transitions) to avoid driver/memory pool races + mutable std::mutex textureUploadMutex; + + // Thread pool for background background tasks (textures, etc.) + std::unique_ptr threadPool; + + // Texture loading progress (for UI) + std::atomic textureTasksScheduled{0}; + std::atomic textureTasksCompleted{0}; + std::atomic loadingFlag{false}; // Default texture resources (used when no texture is provided) TextureResources defaultTextureResources; @@ -632,6 +732,9 @@ class Renderer { bool createSyncObjects(); void cleanupSwapChain(); + + // Ensure Vulkan-Hpp dispatcher is initialized for the current thread when using RAII objects on worker threads + void ensureThreadLocalVulkanInit() const; void recreateSwapChain(); void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index ca9fa3fd..48aaffeb 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -5,6 +5,7 @@ #include #include #include +#include VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file @@ -173,12 +174,48 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + // Initialize background thread pool for async tasks (textures, etc.) AFTER all Vulkan resources are ready + try { + // Size the thread pool based on hardware concurrency, clamped to a sensible range + unsigned int hw = std::max(2u, std::min(8u, std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4u)); + threadPool = std::make_unique(hw); + } catch (const std::exception& e) { + std::cerr << "Failed to create thread pool: " << e.what() << std::endl; + return false; + } + initialized = true; return true; } +void Renderer::ensureThreadLocalVulkanInit() const { + // Initialize Vulkan-Hpp dispatcher per-thread; required for multi-threaded RAII usage + static thread_local bool s_tlsInitialized = false; + if (s_tlsInitialized) return; + try { + vk::detail::DynamicLoader dl; + auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); + if (vkGetInstanceProcAddr) { + VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); + } + if (*instance) { + VULKAN_HPP_DEFAULT_DISPATCHER.init(*instance); + } + if (*device) { + VULKAN_HPP_DEFAULT_DISPATCHER.init(*device); + } + s_tlsInitialized = true; + } catch (...) { + // best-effort + } +} + // Clean up renderer resources void Renderer::Cleanup() { + // Ensure background workers are stopped before tearing down Vulkan resources + if (threadPool) { + threadPool.reset(); + } if (initialized) { std::cout << "Starting renderer cleanup..." << std::endl; @@ -448,7 +485,8 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { std::set uniqueQueueFamilies = { queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.presentFamily.value(), - queueFamilyIndices.computeFamily.value() + queueFamilyIndices.computeFamily.value(), + queueFamilyIndices.transferFamily.value() }; float queuePriority = 1.0f; @@ -522,6 +560,16 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { graphicsQueue = vk::raii::Queue(device, queueFamilyIndices.graphicsFamily.value(), 0); presentQueue = vk::raii::Queue(device, queueFamilyIndices.presentFamily.value(), 0); computeQueue = vk::raii::Queue(device, queueFamilyIndices.computeFamily.value(), 0); + transferQueue = vk::raii::Queue(device, queueFamilyIndices.transferFamily.value(), 0); + + // Create global timeline semaphore for uploads early (needed before default texture creation) + vk::SemaphoreTypeCreateInfo typeInfo{ + .semaphoreType = vk::SemaphoreType::eTimeline, + .initialValue = 0 + }; + vk::SemaphoreCreateInfo timelineCreateInfo{ .pNext = &typeInfo }; + uploadsTimeline = vk::raii::Semaphore(device, timelineCreateInfo); + uploadTimelineValue.store(0, std::memory_order_relaxed); return true; } catch (const std::exception& e) { diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 11ae9003..621c8c81 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -234,6 +234,8 @@ bool Renderer::createSyncObjects() { inFlightFences.emplace_back(device, fenceInfo); } + // Ensure uploads timeline semaphore exists (created early in createLogicalDevice) + // No action needed here unless reinitializing after swapchain recreation. return true; } catch (const std::exception& e) { std::cerr << "Failed to create sync objects: " << e.what() << std::endl; @@ -608,7 +610,15 @@ void Renderer::Render(const std::vector>& entities, Came vk::raii::PipelineLayout* currentLayout = nullptr; std::vector blendedQueue; - // Render each entity + // If loading, skip drawing scene entities (only clear and allow overlay) + bool blockScene = false; + if (imguiSystem) { + // Prefer renderer flag when available + blockScene = IsLoading() || (GetTextureTasksScheduled() > 0 && GetTextureTasksCompleted() < GetTextureTasksScheduled()); + } + + // Render each entity (skip while loading) + if (!blockScene) for (auto const& uptr : entities) { Entity* entity = uptr.get(); if (!entity || !entity->IsActive()) { @@ -1027,11 +1037,20 @@ void Renderer::Render(const std::vector>& entities, Came commandBuffers[currentFrame].end(); // Submit command buffer - vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; + // Wait for both image availability (binary) and all completed texture uploads (timeline) + std::array waitSems = { *imageAvailableSemaphores[currentFrame], *uploadsTimeline }; + std::array waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader }; + uint64_t uploadsValueToWait = uploadTimelineValue.load(std::memory_order_relaxed); + std::array waitValues = { 0ull, uploadsValueToWait }; + vk::TimelineSemaphoreSubmitInfo timelineWaitInfo{ + .waitSemaphoreValueCount = static_cast(waitValues.size()), + .pWaitSemaphoreValues = waitValues.data() + }; vk::SubmitInfo submitInfo{ - .waitSemaphoreCount = 1, - .pWaitSemaphores = &*imageAvailableSemaphores[currentFrame], - .pWaitDstStageMask = waitStages, + .pNext = &timelineWaitInfo, + .waitSemaphoreCount = static_cast(waitSems.size()), + .pWaitSemaphores = waitSems.data(), + .pWaitDstStageMask = waitStages.data(), .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 320a19b2..caba414b 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -8,12 +8,12 @@ #include #include #include +#include // stb_image dependency removed; all GLTF textures are uploaded via memory path from ModelLoader. // KTX2 support #include -#include // This file contains resource-related methods from the Renderer class @@ -65,28 +65,67 @@ bool Renderer::createDepthResources() { // Create texture image bool Renderer::createTextureImage(const std::string& texturePath_, TextureResources& resources) { try { - auto texturePath = const_cast(texturePath_); + ensureThreadLocalVulkanInit(); + const std::string textureId = ResolveTextureId(texturePath_); // Check if texture already exists - auto it = textureResources.find(texturePath); - if (it != textureResources.end()) { - // Texture already loaded and cached; leave cache intact and return success - return true; + { + std::shared_lock texLock(textureResourcesMutex); + auto it = textureResources.find(textureId); + if (it != textureResources.end()) { + // Texture already loaded and cached; leave cache intact and return success + return true; + } + } + + // Resolve on-disk path (may differ from logical ID) + std::string resolvedPath = textureId; + + // Ensure command pool is initialized before any GPU work + if (!*commandPool) { + std::cerr << "createTextureImage: commandPool not initialized yet for '" << textureId << "'" << std::endl; + return false; } + // Per-texture de-duplication (serialize loads of the same texture ID only) + { + std::unique_lock lk(textureLoadStateMutex); + while (texturesLoading.find(textureId) != texturesLoading.end()) { + textureLoadStateCv.wait(lk); + } + } + // Double-check cache after the wait + { + std::shared_lock texLock(textureResourcesMutex); + auto it2 = textureResources.find(textureId); + if (it2 != textureResources.end()) { + return true; + } + } + // Mark as loading and ensure we notify on all exit paths + { + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.insert(textureId); + } + auto _loadingGuard = std::unique_ptr>((void*)1, [this, textureId](void*){ + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.erase(textureId); + textureLoadStateCv.notify_all(); + }); + // Check if this is a KTX2 file - bool isKtx2 = texturePath.find(".ktx2") != std::string::npos; + bool isKtx2 = resolvedPath.find(".ktx2") != std::string::npos; // If it's a KTX2 texture but the path doesn't exist, try common fallback filename variants if (isKtx2) { - std::filesystem::path origPath(texturePath); + std::filesystem::path origPath(resolvedPath); if (!std::filesystem::exists(origPath)) { std::string fname = origPath.filename().string(); std::string dir = origPath.parent_path().string(); auto tryCandidate = [&](const std::string& candidateName) -> bool { std::filesystem::path cand = std::filesystem::path(dir) / candidateName; if (std::filesystem::exists(cand)) { - std::cout << "Resolved missing texture '" << texturePath << "' to existing file '" << cand.string() << "'" << std::endl; - texturePath = cand.string(); + std::cout << "Resolved missing texture '" << resolvedPath << "' to existing file '" << cand.string() << "'" << std::endl; + resolvedPath = cand.string(); return true; } return false; @@ -115,31 +154,32 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour ktxTexture2* ktxTex = nullptr; vk::DeviceSize imageSize; - // Track KTX2 transcoding state and original format across the function scope + // Track KTX2 transcoding state across the function scope (BasisU only) bool wasTranscoded = false; - VkFormat ktxHeaderVkFormat = VK_FORMAT_UNDEFINED; + // Track KTX2 header-provided VkFormat (0 == VK_FORMAT_UNDEFINED) + uint32_t headerVkFormatRaw = 0; uint32_t mipLevels = 1; std::vector copyRegions; if (isKtx2) { // Load KTX2 file - KTX_error_code result = ktxTexture2_CreateFromNamedFile(texturePath.c_str(), + KTX_error_code result = ktxTexture2_CreateFromNamedFile(resolvedPath.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); if (result != KTX_SUCCESS) { // Retry with sibling suffix variants if file exists but cannot be parsed/opened - std::filesystem::path origPath(texturePath); + std::filesystem::path origPath(resolvedPath); std::string fname = origPath.filename().string(); std::string dir = origPath.parent_path().string(); auto tryLoad = [&](const std::string& candidateName) -> bool { std::filesystem::path cand = std::filesystem::path(dir) / candidateName; if (std::filesystem::exists(cand)) { std::string candStr = cand.string(); - std::cout << "Retrying KTX2 load with sibling candidate '" << candStr << "' for original '" << texturePath << "'" << std::endl; + std::cout << "Retrying KTX2 load with sibling candidate '" << candStr << "' for original '" << resolvedPath << "'" << std::endl; result = ktxTexture2_CreateFromNamedFile(candStr.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); if (result == KTX_SUCCESS) { - texturePath = candStr; // Use the successfully opened candidate + resolvedPath = candStr; // Use the successfully opened candidate return true; } } @@ -160,21 +200,23 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour if (loaded) break; } } - assert (result != KTX_SUCCESS); } - // Cache header-provided VkFormat - ktxHeaderVkFormat = static_cast(ktxTex->vkFormat); + // Bail out if we still failed to load + if (result != KTX_SUCCESS || ktxTex == nullptr) { + std::cerr << "Failed to load KTX2 texture: " << resolvedPath << " (error: " << result << ")" << std::endl; + return false; + } - // Check if texture needs transcoding (Basis Universal compressed) + // Read header-provided vkFormat (if already GPU-compressed/transcoded offline) + headerVkFormatRaw = static_cast(ktxTex->vkFormat); + + // Check if the texture needs BasisU transcoding; if so, transcode to RGBA32 wasTranscoded = ktxTexture2_NeedsTranscoding(ktxTex); if (wasTranscoded) { - // Transcode to RGBA8 uncompressed format for Vulkan compatibility - ktx_transcode_fmt_e transcodeFormat = KTX_TTF_RGBA32; - - result = ktxTexture2_TranscodeBasis(ktxTex, transcodeFormat, 0); + result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); if (result != KTX_SUCCESS) { - std::cerr << "Failed to transcode KTX2 texture: " << texturePath << " (error: " << result << ")" << std::endl; + std::cerr << "Failed to transcode KTX2 BasisU texture to RGBA32: " << resolvedPath << " (error: " << result << ")" << std::endl; ktxTexture_Destroy((ktxTexture*)ktxTex); return false; } @@ -182,17 +224,13 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour texWidth = ktxTex->baseWidth; texHeight = ktxTex->baseHeight; - texChannels = 4; // KTX2 textures are typically RGBA + texChannels = 4; // logical channels; compressed size handled below // Disable mipmapping for now - memory pool only supports single mip level // TODO: Implement proper mipmap support in memory pool mipLevels = 1; - // Calculate size for base level only - if (wasTranscoded) { - imageSize = texWidth * texHeight * 4; // RGBA = 4 bytes per pixel - } else { - imageSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); // Only level 0 - } + // Calculate size for base level only (use libktx for correct size incl. compression) + imageSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); // Create single copy region for base level only copyRegions.push_back({ @@ -210,7 +248,7 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour }); } else { // Non-KTX texture loading via file path is disabled to simplify pipeline. - std::cerr << "Unsupported non-KTX2 texture path: " << texturePath << std::endl; + std::cerr << "Unsupported non-KTX2 texture path: " << textureId << std::endl; return false; } @@ -225,24 +263,11 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour void* data = stagingBufferMemory.mapMemory(0, imageSize); if (isKtx2) { - // Copy KTX2 texture data for base level only (level 0) - size_t levelSize; - const void* levelData; - - if (ktxTexture2_NeedsTranscoding(ktxTex)) { - // For transcoded textures, get data from the transcoded buffer - levelSize = texWidth * texHeight * 4; // RGBA = 4 bytes per pixel - ktx_size_t offset; - ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); - levelData = ktxTexture_GetData((ktxTexture*)ktxTex) + offset; - } else { - // For non-transcoded textures, get data directly - levelSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); - ktx_size_t offset; - ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); - levelData = ktxTexture_GetData((ktxTexture*)ktxTex) + offset; - } - + // Copy KTX2 texture data for base level only (level 0), regardless of transcode target + ktx_size_t offset = 0; + ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); + const void* levelData = ktxTexture_GetData((ktxTexture*)ktxTex) + offset; + size_t levelSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); memcpy(data, levelData, levelSize); } else { // Copy regular image data @@ -258,25 +283,26 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour // no-op: non-KTX path disabled } - // Determine appropriate texture format based on texture type and KTX2 metadata + // Determine appropriate texture format vk::Format textureFormat; if (isKtx2) { - if (wasTranscoded) { - // For transcoded Basis to RGBA32, choose by heuristic (sRGB for baseColor/albedo/diffuse) - textureFormat = Renderer::determineTextureFormat(texturePath); - } else { - // Use the VkFormat provided by the KTX2 container if available (from header) - VkFormat vkfmt = ktxHeaderVkFormat; - if (vkfmt == VK_FORMAT_UNDEFINED) { - textureFormat = Renderer::determineTextureFormat(texturePath); + // If the KTX2 provided a valid VkFormat and we did NOT transcode, use it (may be a GPU compressed format) + if (!wasTranscoded) { + VkFormat headerFmt = static_cast(headerVkFormatRaw); + if (headerFmt != VK_FORMAT_UNDEFINED) { + textureFormat = static_cast(headerFmt); } else { - textureFormat = static_cast(vkfmt); + textureFormat = Renderer::determineTextureFormat(textureId); } + } else { + // Transcoded to RGBA32; choose SRGB/UNORM by heuristic + textureFormat = Renderer::determineTextureFormat(textureId); } } else { - textureFormat = Renderer::determineTextureFormat(texturePath); + textureFormat = Renderer::determineTextureFormat(textureId); } + // Create texture image using memory pool auto [textureImg, textureImgAllocation] = createImagePooled( texWidth, @@ -290,32 +316,8 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour resources.textureImage = std::move(textureImg); resources.textureImageAllocation = std::move(textureImgAllocation); - // Transition image layout for copy - transitionImageLayout( - *resources.textureImage, - textureFormat, - vk::ImageLayout::eUndefined, - vk::ImageLayout::eTransferDstOptimal, - mipLevels - ); - - // Copy buffer to image - copyBufferToImage( - *stagingBuffer, - *resources.textureImage, - static_cast(texWidth), - static_cast(texHeight), - copyRegions - ); - - // Transition image layout for shader access - transitionImageLayout( - *resources.textureImage, - textureFormat, - vk::ImageLayout::eTransferDstOptimal, - vk::ImageLayout::eShaderReadOnlyOptimal, - mipLevels - ); + // GPU upload for this texture (no global serialization) + uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, copyRegions, mipLevels); // Store the format and mipLevels for createTextureImageView resources.format = textureFormat; @@ -331,8 +333,11 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour return false; } - // Add to texture resources map - textureResources[texturePath] = std::move(resources); + // Add to texture resources map (guarded) + { + std::unique_lock texLock(textureResourcesMutex); + textureResources[textureId] = std::move(resources); + } return true; } catch (const std::exception& e) { @@ -499,6 +504,7 @@ bool Renderer::createDefaultTextureResources() { // Create texture sampler bool Renderer::createTextureSampler(TextureResources& resources) { try { + ensureThreadLocalVulkanInit(); // Get physical device properties vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); @@ -531,23 +537,30 @@ bool Renderer::createTextureSampler(TextureResources& resources) { // Load texture from file (public wrapper for createTextureImage) bool Renderer::LoadTexture(const std::string& texturePath) { + ensureThreadLocalVulkanInit(); if (texturePath.empty()) { std::cerr << "LoadTexture: Empty texture path provided" << std::endl; return false; } + // Resolve aliases (canonical ID -> actual key) + const std::string resolvedId = ResolveTextureId(texturePath); + // Check if texture is already loaded - auto it = textureResources.find(texturePath); - if (it != textureResources.end()) { - // Texture already loaded - return true; + { + std::shared_lock texLock(textureResourcesMutex); + auto it = textureResources.find(resolvedId); + if (it != textureResources.end()) { + // Texture already loaded + return true; + } } // Create temporary texture resources (unused output; cache will be populated internally) TextureResources tempResources; // Use existing createTextureImage method (it inserts into textureResources on success) - bool success = createTextureImage(texturePath, tempResources); + bool success = createTextureImage(resolvedId, tempResources); if (!success) { std::cerr << "Failed to load texture: " << texturePath << std::endl; @@ -581,17 +594,49 @@ vk::Format Renderer::determineTextureFormat(const std::string& textureId) { // Load texture from raw image data in memory bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigned char* imageData, int width, int height, int channels) { - if (textureId.empty() || !imageData || width <= 0 || height <= 0 || channels <= 0) { + ensureThreadLocalVulkanInit(); + const std::string resolvedId = ResolveTextureId(textureId); + std::cout << "[LoadTextureFromMemory] start id=" << textureId << " -> resolved=" << resolvedId << " size=" << width << "x" << height << " ch=" << channels << std::endl; + if (resolvedId.empty() || !imageData || width <= 0 || height <= 0 || channels <= 0) { std::cerr << "LoadTextureFromMemory: Invalid parameters" << std::endl; return false; } // Check if texture is already loaded - auto it = textureResources.find(textureId); - if (it != textureResources.end()) { - // Texture already loaded - return true; + { + std::shared_lock texLock(textureResourcesMutex); + auto it = textureResources.find(resolvedId); + if (it != textureResources.end()) { + // Texture already loaded + return true; + } + } + + // Per-texture de-duplication (serialize loads of the same texture ID only) + { + std::unique_lock lk(textureLoadStateMutex); + while (texturesLoading.find(resolvedId) != texturesLoading.end()) { + textureLoadStateCv.wait(lk); + } + } + // Double-check cache after the wait + { + std::shared_lock texLock(textureResourcesMutex); + auto it2 = textureResources.find(resolvedId); + if (it2 != textureResources.end()) { + return true; + } } + // Mark as loading and ensure we notify on all exit paths + { + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.insert(resolvedId); + } + auto _loadingGuard = std::unique_ptr>((void*)1, [this, resolvedId](void*){ + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.erase(resolvedId); + textureLoadStateCv.notify_all(); + }); try { TextureResources resources; @@ -648,6 +693,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne // Determine the appropriate texture format based on the texture type vk::Format textureFormat = determineTextureFormat(textureId); + // Create texture image using memory pool auto [textureImg, textureImgAllocation] = createImagePooled( width, @@ -661,15 +707,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne resources.textureImage = std::move(textureImg); resources.textureImageAllocation = std::move(textureImgAllocation); - // Transition image layout for copy - transitionImageLayout( - *resources.textureImage, - textureFormat, - vk::ImageLayout::eUndefined, - vk::ImageLayout::eTransferDstOptimal - ); - - // Copy buffer to image + // GPU upload (no global serialization). Copy buffer to image in a single submit. std::vector regions = {{ .bufferOffset = 0, .bufferRowLength = 0, @@ -683,25 +721,14 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne .imageOffset = {0, 0, 0}, .imageExtent = {static_cast(width), static_cast(height), 1} }}; - copyBufferToImage( - *stagingBuffer, - *resources.textureImage, - static_cast(width), - static_cast(height), - regions - ); - - // Transition image layout for shader access - transitionImageLayout( - *resources.textureImage, - textureFormat, - vk::ImageLayout::eTransferDstOptimal, - vk::ImageLayout::eShaderReadOnlyOptimal - ); + uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, regions, 1); // Store the format for createTextureImageView resources.format = textureFormat; + // Use resolvedId as the cache key to avoid duplicates + const std::string& cacheId = resolvedId; + // Create texture image view resources.textureImageView = createImageView( resources.textureImage, @@ -714,10 +741,13 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne return false; } - // Add to texture resources map - textureResources[textureId] = std::move(resources); + // Add to texture resources map (guarded) + { + std::unique_lock texLock(textureResourcesMutex); + textureResources[cacheId] = std::move(resources); + } - std::cout << "Successfully loaded texture from memory: " << textureId + std::cout << "Successfully loaded texture from memory: " << cacheId << " (" << width << "x" << height << ", " << channels << " channels)" << std::endl; return true; @@ -729,6 +759,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne // Create mesh resources bool Renderer::createMeshResources(MeshComponent* meshComponent) { + ensureThreadLocalVulkanInit(); try { // Check if mesh resources already exist auto it = meshResources.find(meshComponent); @@ -811,6 +842,7 @@ bool Renderer::createMeshResources(MeshComponent* meshComponent) { // Create uniform buffers bool Renderer::createUniformBuffers(Entity* entity) { + ensureThreadLocalVulkanInit(); try { // Check if entity resources already exist auto it = entityResources.find(entity); @@ -942,6 +974,10 @@ bool Renderer::createDescriptorPool() { // Create descriptor sets bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePath, bool usePBR) { + // Resolve alias before taking the shared lock to avoid nested shared_lock on the same mutex + const std::string resolvedTexturePath = ResolveTextureId(texturePath); + // Guard textureResources access while resolving texture handles + std::shared_lock texLock(textureResourcesMutex); try { // Get entity resources auto entityIt = entityResources.find(entity); @@ -953,7 +989,7 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa // Get texture resources - use default texture as fallback if specific texture fails TextureResources* textureRes = nullptr; if (!texturePath.empty()) { - auto textureIt = textureResources.find(texturePath); + auto textureIt = textureResources.find(resolvedTexturePath); if (textureIt == textureResources.end()) { // If this is a GLTF embedded texture ID, don't try to load from disk if (texturePath.rfind("gltf_", 0) == 0) { @@ -985,8 +1021,7 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa textureRes = &defaultTextureResources; } } else { - std::cerr << "Warning: On-demand texture loading disabled for " << texturePath - << "; using default texture instead" << std::endl; + // Texture not yet available; bind default texture for now. textureRes = &defaultTextureResources; } } else { @@ -1157,9 +1192,10 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa // Create image infos for each PBR texture binding for (int j = 0; j < 5; j++) { const std::string& currentTexturePath = pbrTexturePaths[j]; + const std::string resolvedBindingPath = ResolveTextureId(currentTexturePath); // Find the texture resources for this binding - auto textureIt = textureResources.find(currentTexturePath); + auto textureIt = textureResources.find(resolvedBindingPath); if (textureIt != textureResources.end()) { // Use the specific texture for this binding const auto& texRes = textureIt->second; @@ -1466,10 +1502,16 @@ std::pair Renderer::createBuffer( // Copy buffer void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size) { + ensureThreadLocalVulkanInit(); try { - // Create command buffer using RAII + // Create a temporary transient command pool and command buffer to isolate per-thread usage (transfer family) + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); vk::CommandBufferAllocateInfo allocInfo{ - .commandPool = *commandPool, + .commandPool = *tempPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; @@ -1502,12 +1544,13 @@ void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuff .pCommandBuffers = &*commandBuffer }; - // Use mutex to ensure thread-safe access to graphics queue + // Use mutex to ensure thread-safe access to transfer queue + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, nullptr); - graphicsQueue.waitIdle(); + transferQueue.submit(submitInfo, *fence); } + device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); } catch (const std::exception& e) { std::cerr << "Failed to copy buffer: " << e.what() << std::endl; throw; @@ -1574,7 +1617,6 @@ std::pair> Renderer::cr // Use memory pool for allocation (mipmap support limited by memory pool API) auto [image, allocation] = memoryPool->createImage(width, height, format, tiling, usage, properties); - std::cout << "Created image using memory pool: " << width << "x" << height << " format=" << static_cast(format) << " mipLevels=" << mipLevels << std::endl; return {std::move(image), std::move(allocation)}; @@ -1587,6 +1629,7 @@ std::pair> Renderer::cr // Create an image view vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels) { try { + ensureThreadLocalVulkanInit(); // Create image view vk::ImageViewCreateInfo viewInfo{ .image = *image, @@ -1610,10 +1653,16 @@ vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format // Transition image layout void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels) { + ensureThreadLocalVulkanInit(); try { - // Create a command buffer using RAII + // Create a temporary transient command pool and command buffer to isolate per-thread usage + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); vk::CommandBufferAllocateInfo allocInfo{ - .commandPool = *commandPool, + .commandPool = *tempPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; @@ -1687,22 +1736,24 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima nullptr, barrier ); + std::cout << "[transitionImageLayout] recorded barrier image=" << (void*)image << " old=" << (int)oldLayout << " new=" << (int)newLayout << std::endl; // End command buffer commandBuffer.end(); // Submit command buffer - // Use mutex to ensure thread-safe access to the graphics queue + // Submit transition; protect submit with mutex but wait outside + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffer - }; - std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, nullptr); - graphicsQueue.waitIdle(); + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *fence); } + device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); } catch (const std::exception& e) { std::cerr << "Failed to transition image layout: " << e.what() << std::endl; throw; @@ -1711,10 +1762,16 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima // Copy buffer to image void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector& regions) const { + ensureThreadLocalVulkanInit(); try { - // Create a command buffer using RAII + // Create a temporary transient command pool for the transfer queue + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); vk::CommandBufferAllocateInfo allocInfo{ - .commandPool = *commandPool, + .commandPool = *tempPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; @@ -1736,6 +1793,7 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi vk::ImageLayout::eTransferDstOptimal, regions ); + std::cout << "[copyBufferToImage] recorded copy img=" << (void*)image << std::endl; // End command buffer commandBuffer.end(); @@ -1746,12 +1804,13 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi .pCommandBuffers = &*commandBuffer }; - // Use mutex to ensure thread-safe access to graphics queue + // Protect submit with queue mutex, wait outside + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, nullptr); - graphicsQueue.waitIdle(); + transferQueue.submit(submitInfo, *fence); } + device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); } catch (const std::exception& e) { std::cerr << "Failed to copy buffer to image: " << e.what() << std::endl; throw; @@ -1941,3 +2000,149 @@ bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector Renderer::LoadTextureAsync(const std::string& texturePath) { + if (texturePath.empty()) { + return std::async(std::launch::deferred, [] { return false; }); + } + // Schedule the load without dropping tasks; throttling is handled during GPU upload + textureTasksScheduled.fetch_add(1, std::memory_order_relaxed); + auto task = [this, texturePath]() { + bool ok = this->LoadTexture(texturePath); + textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); + return ok; + }; + if (!threadPool) { + return std::async(std::launch::async, task); + } + return threadPool->enqueue(task); +} + +std::future Renderer::LoadTextureFromMemoryAsync(const std::string& textureId, const unsigned char* imageData, + int width, int height, int channels) { + if (!imageData || textureId.empty() || width <= 0 || height <= 0 || channels <= 0) { + return std::async(std::launch::deferred, [] { return false; }); + } + // Copy the source bytes so the caller can free/modify their buffer immediately + size_t srcSize = static_cast(width) * static_cast(height) * static_cast(channels); + std::vector dataCopy(srcSize); + std::memcpy(dataCopy.data(), imageData, srcSize); + + textureTasksScheduled.fetch_add(1, std::memory_order_relaxed); + auto task = [this, textureId, data = std::move(dataCopy), width, height, channels]() mutable { + bool ok = this->LoadTextureFromMemory(textureId, data.data(), width, height, channels); + textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); + return ok; + }; + if (!threadPool) { + return std::async(std::launch::async, std::move(task)); + } + return threadPool->enqueue(std::move(task)); +} + + +// Record both layout transitions and the copy in a single submission with a fence +void Renderer::uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + const std::vector& regions, + uint32_t mipLevels) { + ensureThreadLocalVulkanInit(); + try { + // Use a temporary transient command pool for the transfer queue family + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + vk::raii::CommandBuffers cbs(device, allocInfo); + vk::raii::CommandBuffer& cb = cbs[0]; + + vk::CommandBufferBeginInfo beginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; + cb.begin(beginInfo); + + // Barrier: Undefined -> TransferDstOptimal + vk::ImageMemoryBarrier toTransfer{ + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = (format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint) + ? vk::ImageAspectFlagBits::eDepth + : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + toTransfer.srcAccessMask = vk::AccessFlagBits::eNone; + toTransfer.dstAccessMask = vk::AccessFlagBits::eTransferWrite; + cb.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlagBits::eByRegion, + nullptr, nullptr, toTransfer); + + // Copy + cb.copyBufferToImage(staging, image, vk::ImageLayout::eTransferDstOptimal, regions); + + // Barrier: TransferDstOptimal -> ShaderReadOnlyOptimal + vk::ImageMemoryBarrier toShader{ + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = (format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint) + ? vk::ImageAspectFlagBits::eDepth + : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + toShader.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + toShader.dstAccessMask = vk::AccessFlagBits::eNone; // cannot use ShaderRead on transfer queue; visibility handled via timeline wait on graphics + cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlagBits::eByRegion, + nullptr, nullptr, toShader); + + cb.end(); + + // Submit once on the transfer queue, signal timeline semaphore, and wait fence (for safety) + uint64_t signalValue = uploadTimelineValue.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue + }; + vk::SubmitInfo submit{ + .pNext = &timelineInfo, + .commandBufferCount = 1, + .pCommandBuffers = &*cb, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*uploadsTimeline + }; + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + // Prefer dedicated transfer queue + { + std::lock_guard lock(queueMutex); + transferQueue.submit(submit, *fence); + } + device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + } catch (const std::exception& e) { + std::cerr << "uploadImageFromStaging failed: " << e.what() << std::endl; + throw; + } +} diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp index d299b727..5b98b0a4 100644 --- a/attachments/simple_engine/renderer_utils.cpp +++ b/attachments/simple_engine/renderer_utils.cpp @@ -120,29 +120,39 @@ QueueFamilyIndices Renderer::findQueueFamilies(const vk::raii::PhysicalDevice& d // Get queue family properties std::vector queueFamilies = device.getQueueFamilyProperties(); - // Find queue families that support graphics, compute, and present + // Find queue families that support graphics, compute, present, and (optionally) a dedicated transfer queue for (uint32_t i = 0; i < queueFamilies.size(); i++) { + const auto& qf = queueFamilies[i]; // Check for graphics support - if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) { + if ((qf.queueFlags & vk::QueueFlagBits::eGraphics) && !indices.graphicsFamily.has_value()) { indices.graphicsFamily = i; } - // Check for compute support - if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eCompute) { + if ((qf.queueFlags & vk::QueueFlagBits::eCompute) && !indices.computeFamily.has_value()) { indices.computeFamily = i; } - // Check for present support - if (device.getSurfaceSupportKHR(i, surface)) { + if (!indices.presentFamily.has_value() && device.getSurfaceSupportKHR(i, surface)) { indices.presentFamily = i; } - - // If all queue families are found, break - if (indices.isComplete()) { + // Prefer a dedicated transfer queue (transfer bit set, but NOT graphics) if available + if ((qf.queueFlags & vk::QueueFlagBits::eTransfer) && !(qf.queueFlags & vk::QueueFlagBits::eGraphics)) { + if (!indices.transferFamily.has_value()) { + indices.transferFamily = i; + } + } + // If all required queue families are found, we can still continue to try find a dedicated transfer queue + if (indices.isComplete() && indices.transferFamily.has_value()) { + // Found everything including dedicated transfer break; } } + // Fallback: if no dedicated transfer queue, reuse graphics queue for transfer + if (!indices.transferFamily.has_value() && indices.graphicsFamily.has_value()) { + indices.transferFamily = indices.graphicsFamily; + } + return indices; } diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index ebd446de..79774080 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -47,8 +47,11 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, if (!modelLoader || !renderer) { std::cerr << "Error: ModelLoader or Renderer is null" << std::endl; + if (renderer) { renderer->SetLoading(false); } return; } + // Ensure loading flag is cleared on any exit from this function + struct LoadingGuard { Renderer* r; ~LoadingGuard(){ if (r) r->SetLoading(false); } } loadingGuard{renderer}; // Extract model name from file path for entity naming std::filesystem::path modelFilePath(modelPath); @@ -228,14 +231,21 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Use mesh collision shape for accurate geometry interaction PhysicsSystem* physicsSystem = engine->GetPhysicsSystem(); if (physicsSystem) { - RigidBody* rigidBody = physicsSystem->CreateRigidBody(materialEntity, CollisionShape::Mesh, 0.0f); // Mass 0 = static - if (rigidBody) { - rigidBody->SetKinematic(true); // Static geometry doesn't move - rigidBody->SetRestitution(0.15f); // Very low bounce - balls lose 85%+ momentum - rigidBody->SetFriction(0.5f); // Moderate friction - std::cout << "Created physics body for geometry entity: " << entityName << std::endl; + // Only create a physics body if the mesh has valid geometry + auto* mc = materialEntity->GetComponent(); + if (mc && !mc->GetVertices().empty() && !mc->GetIndices().empty()) { + // Queue rigid body creation to the main thread to avoid races + physicsSystem->EnqueueRigidBodyCreation( + materialEntity, + CollisionShape::Mesh, + 0.0f, // mass 0 = static + true, // kinematic + 0.15f, // restitution + 0.5f // friction + ); + std::cout << "Queued physics body for geometry entity: " << entityName << std::endl; } else { - std::cerr << "Failed to create physics body for entity: " << entityName << std::endl; + std::cerr << "Skipping physics body for entity (no geometry): " << entityName << std::endl; } } From 0ef03168e41bc748dcc3693dd4864634d9c09373 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 17 Oct 2025 01:35:15 -0700 Subject: [PATCH 100/102] Working through some of the PR comments. Concentrating on the text of the tutorial section first as only 90 total comments there, 200+ in the attachments and there's still bugs in the game engine itself. Holding back game code changes until it's bug free. --- .../02_math_foundations.adoc | 6 +-- .../03_camera_implementation.adoc | 53 +++++++++++-------- .../05_vulkan_integration.adoc | 14 +++-- .../Engine_Architecture/01_introduction.adoc | 2 +- .../03_component_systems.adoc | 7 --- .../04_resource_management.adoc | 28 ++++++---- 6 files changed, 61 insertions(+), 49 deletions(-) diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc index f217cb6e..5d67e4db 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc @@ -29,7 +29,7 @@ In our camera system, vectors serve several critical purposes: * *Scalar Multiplication*: Used for scaling movements and directions - Example: Slowing down camera movement by multiplying velocity by a factor < 1 -* *Dot Product*: Calculates the cosine of the angle between vectors (when normalized) +* *Dot Product*: Calculates the cosine of the angle between normalized vectors. - Applications: Determining if objects are facing the camera, calculating lighting intensity ==== The Right-Hand Rule @@ -524,7 +524,7 @@ struct Sphere { bool rayIntersectsSphere(const Ray& ray, const Sphere& sphere, float& t) { // Vector from ray origin to sphere center - glm::vec3 oc = ray.origin - sphere.center; + glm::vec3 oc = sphere.center - ray.origin; // Quadratic equation coefficients float a = glm::dot(ray.direction, ray.direction); // Always 1 if direction is normalized @@ -710,7 +710,7 @@ For complex scenes with many objects, raycasting can become computationally expe * *Ray Batching*: Process multiple rays together to take advantage of SIMD instructions -* *Early Termination*: Stop testing once you've found the closest intersection (if that's all you need) +* *Early Termination*: Stop testing once you've found any intersection (if that's all you need) === Projection in 3D Graphics diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc index d54bf086..41ee7a0d 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc @@ -11,7 +11,7 @@ Now that we understand the mathematical foundations, let's implement a flexible There are several types of cameras commonly used in 3D applications: * *First-Person Camera*: Simulates viewing the world through the eyes of a character. -* *Third-Person Camera*: Follows a character from behind or another fixed position. +* *Third-Person Camera*: Follows a character from behind or another fixed relative position. * *Orbit Camera*: Rotates around a fixed point, useful for object inspection. * *Free Camera*: Allows unrestricted movement in all directions. @@ -27,7 +27,7 @@ class Camera { private: // Camera position and orientation glm::vec3 position; - glm::vec3 front; + glm::vec3 forward; glm::vec3 up; glm::vec3 right; glm::vec3 worldUp; @@ -40,6 +40,9 @@ private: float movementSpeed; float mouseSensitivity; float zoom; + float aspectRatio; + float nearPlane; + float farPlane; // Update camera vectors based on Euler angles void updateCameraVectors(); @@ -57,16 +60,16 @@ public: glm::mat4 getViewMatrix() const; // Get projection matrix - glm::mat4 getProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 100.0f) const; + glm::mat4 getProjectionMatrix() const; - // Process keyboard input for camera movement - void processKeyboard(CameraMovement direction, float deltaTime); + // Process camera movement (translation) + void move(CameraMovement direction, float deltaTime); - // Process mouse movement for camera rotation - void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); + // Process camera rotation + void rotate(float xOffset, float yOffset, bool constrainPitch = true); - // Process mouse scroll for zoom - void processMouseScroll(float yOffset); + // Process zoom + void zoom(float delta); // Getters for camera properties glm::vec3 getPosition() const { return position; } @@ -98,18 +101,26 @@ And implement the movement logic: void Camera::processKeyboard(CameraMovement direction, float deltaTime) { float velocity = movementSpeed * deltaTime; - if (direction == CameraMovement::FORWARD) - position += front * velocity; - if (direction == CameraMovement::BACKWARD) - position -= front * velocity; - if (direction == CameraMovement::LEFT) - position -= right * velocity; - if (direction == CameraMovement::RIGHT) - position += right * velocity; - if (direction == CameraMovement::UP) - position += up * velocity; - if (direction == CameraMovement::DOWN) - position -= up * velocity; + switch (direction) { + case CameraMovement::FORWARD: + position += front * velocity; + break; + case CameraMovement::BACKWARD: + position -= front * velocity; + break; + case CameraMovement::LEFT: + position -= right * velocity; + break; + case CameraMovement::RIGHT: + position += right * velocity; + break; + case CameraMovement::UP: + position += up * velocity; + break; + case CameraMovement::DOWN: + position -= up * velocity; + break; + } } ---- diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc index 2f0e8576..7be17f10 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -69,15 +69,13 @@ std::array uniformBuffers; void createUniformBuffers() { vk::DeviceSize bufferSize = sizeof(UniformBufferObject); - + // Create the buffer + vk::BufferCreateInfo bufferInfo{ + .size = bufferSize, + .usage = vk::BufferUsageFlagBits::eUniformBuffer, + .sharingMode = vk::SharingMode::eExclusive + }; for (size_t i = 0; i < maxConcurrentFrames; i++) { - // Create the buffer - vk::BufferCreateInfo bufferInfo{ - .size = bufferSize, - .usage = vk::BufferUsageFlagBits::eUniformBuffer, - .sharingMode = vk::SharingMode::eExclusive - }; - uniformBuffers[i].buffer = vk::raii::Buffer(device, bufferInfo); // Allocate and bind memory diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc index 2de95723..a88f2b3b 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -35,7 +35,7 @@ This chapter builds directly on the foundation established in the main Vulkan tu Beyond Vulkan knowledge, you'll benefit from familiarity with object-oriented programming principles, as modern engine architecture relies heavily on encapsulation, inheritance, and polymorphism to manage complexity. Experience with common design patterns like Observer, Factory, and Singleton will help you recognize when and why we apply these patterns in our engine design. -Modern C++ features play a crucial role in our implementation approach. Smart pointers help us manage resource lifetimes safely, templates enable flexible, reusable components, and other C++11/14/17 features allow us to write more expressive and maintainable code. If you're not comfortable with these concepts, consider reviewing them before proceeding. +Modern C:pp: features play a crucial role in our implementation approach. Smart pointers help us manage resource lifetimes safely, templates enable flexible, reusable components, and other C:pp:11/14/17 features allow us to write more expressive and maintainable code. If you're not comfortable with these concepts, consider reviewing them before proceeding. You should also be familiar with the following chapters from the main tutorial: diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc index b77b20f4..a717797f 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc @@ -102,13 +102,6 @@ public: T* AddComponent(Args&&... args) { static_assert(std::is_base_of::value, "T must derive from Component"); - // Check if component of this type already exists - for (auto& component : components) { - if (dynamic_cast(component.get())) { - return dynamic_cast(component.get()); - } - } - // Create new component auto component = std::make_unique(std::forward(args)...); T* componentPtr = component.get(); diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc index 735a2c0b..920183f4 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc @@ -95,14 +95,19 @@ public: bool IsLoaded() const { return loaded; } // Virtual interface for resource-specific loading and unloading behavior - virtual bool Load() { - loaded = true; - return true; + bool Load() { + loaded = doLoad(); + return loaded; } - virtual void Unload() { + void Unload() { + doUnload(); loaded = false; } + + protected: + virtual bool doLoad() = 0; + virtual bool doUnload() = 0; }; ---- @@ -124,9 +129,14 @@ private: std::unordered_map>> resources; - // Reference counting system for automatic resource lifecycle management - // Maps resource IDs to their current usage count for garbage collection - std::unordered_map refCounts; + // Two-level reference counting system for automatic resource lifecycle management + // First level maps resource type, second level maps resource IDs to their data + struct ResourceData { + std::shared_ptr resource; // The actual resource + int refCount; // Reference count for this resource + }; + std::unordered_map> refCounts; ---- The storage architecture uses a sophisticated two-level mapping system that solves several critical problems in resource management. The outer map keyed by `std::type_index` ensures complete type separation, preventing name collisions between different resource types. For example, you could have both a texture named "stone" and a sound effect named "stone" without conflicts, as they're stored in separate type-specific containers. @@ -201,8 +211,8 @@ After that, we provide the interface for safely accessing cached resources with template bool HasResource(const std::string& resourceId) { // Efficient existence check without resource access overhead - auto& typeResources = resources[std::type_index(typeid(T))]; - return typeResources.find(resourceId) != typeResources.end(); + auto resourceIt = resources.find(std::type_index(typeid(T))); + return resourceIt != resources.end(); } ---- From 277a8178d7083b877aab88307ab2386ffab6875a Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 17 Oct 2025 02:06:19 -0700 Subject: [PATCH 101/102] Down to 78 comments left outstanding from the PR in the tutorial text. IDE ran out of memory trying to read them which means it's time for bed. Will do more tomorrow. --- .../03_camera_implementation.adoc | 494 ------------------ .../04_transformation_matrices.adoc | 175 ------- .../Camera_Transformations/index.adoc | 4 +- .../04_resource_management.adoc | 16 +- .../05_rendering_pipeline.adoc | 6 +- .../GUI/02_imgui_setup.adoc | 2 +- 6 files changed, 19 insertions(+), 678 deletions(-) delete mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc delete mode 100644 en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc deleted file mode 100644 index 41ee7a0d..00000000 --- a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc +++ /dev/null @@ -1,494 +0,0 @@ -:pp: {plus}{plus} - -= Camera & Transformations: Camera Implementation - -== Camera Implementation - -Now that we understand the mathematical foundations, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. This implementation is designed for a general-purpose 3D application or game engine, and the concepts can be applied to various types of applications, from first-person games to architectural visualization tools. - -=== Camera Types - -There are several types of cameras commonly used in 3D applications: - -* *First-Person Camera*: Simulates viewing the world through the eyes of a character. -* *Third-Person Camera*: Follows a character from behind or another fixed relative position. -* *Orbit Camera*: Rotates around a fixed point, useful for object inspection. -* *Free Camera*: Allows unrestricted movement in all directions. - -For our implementation, we'll focus on a versatile camera that can be configured for different use cases. - -=== Camera Class Design - -Let's design a camera class that encapsulates the necessary functionality: - -[source,cpp] ----- -class Camera { -private: - // Camera position and orientation - glm::vec3 position; - glm::vec3 forward; - glm::vec3 up; - glm::vec3 right; - glm::vec3 worldUp; - - // Euler angles - float yaw; - float pitch; - - // Camera options - float movementSpeed; - float mouseSensitivity; - float zoom; - float aspectRatio; - float nearPlane; - float farPlane; - - // Update camera vectors based on Euler angles - void updateCameraVectors(); - -public: - // Constructor with default values - Camera( - glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), - glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), - float yaw = -90.0f, - float pitch = 0.0f - ); - - // Get view matrix - glm::mat4 getViewMatrix() const; - - // Get projection matrix - glm::mat4 getProjectionMatrix() const; - - // Process camera movement (translation) - void move(CameraMovement direction, float deltaTime); - - // Process camera rotation - void rotate(float xOffset, float yOffset, bool constrainPitch = true); - - // Process zoom - void zoom(float delta); - - // Getters for camera properties - glm::vec3 getPosition() const { return position; } - glm::vec3 getFront() const { return front; } - float getZoom() const { return zoom; } -}; ----- - -=== Camera Movement - -We'll define an enum for camera movement directions: - -[source,cpp] ----- -enum class CameraMovement { - FORWARD, - BACKWARD, - LEFT, - RIGHT, - UP, - DOWN -}; ----- - -And implement the movement logic: - -[source,cpp] ----- -void Camera::processKeyboard(CameraMovement direction, float deltaTime) { - float velocity = movementSpeed * deltaTime; - - switch (direction) { - case CameraMovement::FORWARD: - position += front * velocity; - break; - case CameraMovement::BACKWARD: - position -= front * velocity; - break; - case CameraMovement::LEFT: - position -= right * velocity; - break; - case CameraMovement::RIGHT: - position += right * velocity; - break; - case CameraMovement::UP: - position += up * velocity; - break; - case CameraMovement::DOWN: - position -= up * velocity; - break; - } -} ----- - -==== Handling Input Events - -The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW, (a common windowing library used with Vulkan): - -[source,cpp] ----- -// In your application's input handling function -void processInput(GLFWwindow* window, Camera& camera, float deltaTime) { - // Keyboard input for camera movement - if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::FORWARD, deltaTime); - if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); - if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::LEFT, deltaTime); - if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::RIGHT, deltaTime); - if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::UP, deltaTime); - if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::DOWN, deltaTime); -} - -// Mouse callback function for camera rotation -void mouseCallback(GLFWwindow* window, double xpos, double ypos) { - static bool firstMouse = true; - static float lastX = 0.0f, lastY = 0.0f; - - if (firstMouse) { - lastX = xpos; - lastY = ypos; - firstMouse = false; - } - - float xoffset = xpos - lastX; - float yoffset = lastY - ypos; // Reversed: y ranges bottom to top - - lastX = xpos; - lastY = ypos; - - // Pass the mouse movement to the camera - camera.processMouseMovement(xoffset, yoffset); -} - -// Scroll callback for zoom -void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { - camera.processMouseScroll(yoffset); -} - -// Setting up the callbacks in your initialization code -void setupInputCallbacks(GLFWwindow* window) { - glfwSetCursorPosCallback(window, mouseCallback); - glfwSetScrollCallback(window, scrollCallback); - glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // Capture mouse -} ----- - -[NOTE] -==== -The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. -==== - -=== Camera Rotation - -For camera rotation, we'll use mouse input to adjust the yaw and pitch angles: - -[source,cpp] ----- -void Camera::processMouseMovement(float xOffset, float yOffset, bool constrainPitch) { - xOffset *= mouseSensitivity; - yOffset *= mouseSensitivity; - - yaw += xOffset; - pitch += yOffset; - - // Constrain pitch to avoid flipping - if (constrainPitch) { - if (pitch > 89.0f) - pitch = 89.0f; - if (pitch < -89.0f) - pitch = -89.0f; - } - - // Update camera vectors based on updated Euler angles - updateCameraVectors(); -} ----- - -=== Updating Camera Vectors - -After changing the camera's orientation, we need to recalculate the front, right, and up vectors: - -[source,cpp] ----- -void Camera::updateCameraVectors() { - // Calculate the new front vector - glm::vec3 newFront; - newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); - newFront.y = sin(glm::radians(pitch)); - newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); - front = glm::normalize(newFront); - - // Recalculate the right and up vectors - right = glm::normalize(glm::cross(front, worldUp)); - up = glm::normalize(glm::cross(right, front)); -} ----- - -=== View Matrix - -The view matrix transforms world coordinates into view coordinates (camera space): - -[source,cpp] ----- -glm::mat4 Camera::getViewMatrix() const { - return glm::lookAt(position, position + front, up); -} ----- - -=== Projection Matrix - -The projection matrix transforms view coordinates into clip coordinates: - -[source,cpp] ----- -glm::mat4 Camera::getProjectionMatrix(float aspectRatio, float nearPlane, float farPlane) const { - return glm::perspective(glm::radians(zoom), aspectRatio, nearPlane, farPlane); -} ----- - -=== Advanced Topics: Third-Person Camera Implementation - -In this section, we'll explore advanced techniques for implementing a third-person camera that follows a character while avoiding occlusion and maintaining focus on the character. - -==== Third-Person Camera Design - -A third-person camera typically needs to: - -1. Follow the character at a specified distance -2. Maintain a consistent view of the character -3. Avoid being occluded by objects in the environment -4. Provide smooth transitions during movement and rotation - -Let's extend our camera class to support these features: - -[source,cpp] ----- -class ThirdPersonCamera : public Camera { -private: - // Target (character) properties - glm::vec3 targetPosition; - glm::vec3 targetForward; - - // Camera configuration - float followDistance; - float followHeight; - float followSmoothness; - - // Occlusion avoidance - float minDistance; - float raycastDistance; - - // Internal state - glm::vec3 desiredPosition; - glm::vec3 smoothDampVelocity; - -public: - ThirdPersonCamera( - float followDistance = 5.0f, - float followHeight = 2.0f, - float followSmoothness = 0.1f, - float minDistance = 1.0f - ); - - // Update camera position based on target - void updatePosition(const glm::vec3& targetPos, const glm::vec3& targetFwd, float deltaTime); - - // Handle occlusion avoidance - void handleOcclusion(const Scene& scene); - - // Orbit around target - void orbit(float horizontalAngle, float verticalAngle); - - // Setters for camera properties - void setFollowDistance(float distance) { followDistance = distance; } - void setFollowHeight(float height) { followHeight = height; } - void setFollowSmoothness(float smoothness) { followSmoothness = smoothness; } -}; ----- - -==== Character Following Algorithm - -The core of a third-person camera is the algorithm that positions the camera relative to the character. Here's an implementation of the `updatePosition` method: - -[source,cpp] ----- -void ThirdPersonCamera::updatePosition( - const glm::vec3& targetPos, - const glm::vec3& targetFwd, - float deltaTime -) { - // Update target properties - targetPosition = targetPos; - targetForward = glm::normalize(targetFwd); - - // Calculate the desired camera position - // Position the camera behind and above the character - glm::vec3 offset = -targetForward * followDistance; - offset.y = followHeight; - - desiredPosition = targetPosition + offset; - - // Smooth camera movement using exponential smoothing - position = glm::mix(position, desiredPosition, 1.0f - pow(followSmoothness, deltaTime * 60.0f)); - - // Update the camera to look at the target - front = glm::normalize(targetPosition - position); - - // Recalculate right and up vectors - right = glm::normalize(glm::cross(front, worldUp)); - up = glm::normalize(glm::cross(right, front)); -} ----- - -This implementation: - -1. Positions the camera behind the character based on the character's forward direction -2. Adds height to give a better view of the character and surroundings -3. Uses exponential smoothing to create natural camera movement -4. Always keeps the camera focused on the character - -==== Occlusion Avoidance - -One of the most challenging aspects of a third-person camera is handling occlusion—when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: - -[source,cpp] ----- -void ThirdPersonCamera::handleOcclusion(const Scene& scene) { - // Cast a ray from the target to the desired camera position - Ray ray; - ray.origin = targetPosition; - ray.direction = glm::normalize(desiredPosition - targetPosition); - - // Check for intersections with scene objects - RaycastHit hit; - if (scene.raycast(ray, hit, glm::length(desiredPosition - targetPosition))) { - // If there's an intersection, move the camera to the hit point - // minus a small offset to avoid clipping - float offsetDistance = 0.2f; - position = hit.point - (ray.direction * offsetDistance); - - // Ensure we don't get too close to the target - float currentDistance = glm::length(position - targetPosition); - if (currentDistance < minDistance) { - position = targetPosition + ray.direction * minDistance; - } - - // Update the camera to look at the target - front = glm::normalize(targetPosition - position); - right = glm::normalize(glm::cross(front, worldUp)); - up = glm::normalize(glm::cross(right, front)); - } -} ----- - -This implementation: - -1. Casts a ray from the character to the desired camera position -2. If the ray hits an object, moves the camera to the hit point (with a small offset) -3. Ensures the camera doesn't get too close to the character -4. Updates the camera orientation to maintain focus on the character - -===== Performance Considerations for Occlusion Avoidance - -When implementing occlusion avoidance, be mindful of performance: - -* *Use simplified collision geometry*: For raycasting, use simpler collision shapes than your rendering geometry -* *Limit the frequency of occlusion checks*: You may not need to check every frame on slower devices -* *Consider spatial partitioning*: Use structures like octrees to speed up raycasts by quickly eliminating objects that can't possibly intersect with the ray -* *Optimize for mobile platforms*: For performance-constrained devices, consider simplifying the occlusion algorithm or reducing its precision - -==== Implementing Orbit Controls - -Many third-person games allow the player to orbit the camera around the character. Here's how to implement this functionality: - -[source,cpp] ----- -void ThirdPersonCamera::orbit(float horizontalAngle, float verticalAngle) { - // Update yaw and pitch based on input - yaw += horizontalAngle; - pitch += verticalAngle; - - // Constrain pitch to avoid flipping - if (pitch > 89.0f) - pitch = 89.0f; - if (pitch < -89.0f) - pitch = -89.0f; - - // Calculate the new camera position based on spherical coordinates - float radius = followDistance; - float yawRad = glm::radians(yaw); - float pitchRad = glm::radians(pitch); - - // Convert spherical coordinates to Cartesian - glm::vec3 offset; - offset.x = radius * cos(yawRad) * cos(pitchRad); - offset.y = radius * sin(pitchRad); - offset.z = radius * sin(yawRad) * cos(pitchRad); - - // Set the desired position - desiredPosition = targetPosition + offset; - - // Update camera vectors - front = glm::normalize(targetPosition - desiredPosition); - right = glm::normalize(glm::cross(front, worldUp)); - up = glm::normalize(glm::cross(right, front)); -} ----- - -This implementation: - -1. Updates the camera's yaw and pitch based on user input -2. Constrains the pitch to prevent the camera from flipping -3. Calculates a new camera position using spherical coordinates -4. Keeps the camera focused on the character - -==== Integrating with Character Movement - -To create a complete third-person camera system, we need to integrate it with character movement. Here's an example of how to use the third-person camera in a game loop: - -[source,cpp] ----- -void gameLoop(float deltaTime) { - // Update character position and orientation based on input - character.update(deltaTime); - - // Update camera position to follow the character - thirdPersonCamera.updatePosition( - character.getPosition(), - character.getForward(), - deltaTime - ); - - // Handle camera occlusion - thirdPersonCamera.handleOcclusion(scene); - - // Process camera orbit input (if any) - if (mouseInputDetected) { - thirdPersonCamera.orbit(mouseDeltaX, mouseDeltaY); - } - - // Get the view and projection matrices for rendering - glm::mat4 viewMatrix = thirdPersonCamera.getViewMatrix(); - glm::mat4 projMatrix = thirdPersonCamera.getProjectionMatrix(aspectRatio); - - // Use these matrices for rendering the scene - renderer.render(scene, viewMatrix, projMatrix); -} ----- - -[NOTE] -==== -For more advanced camera techniques, refer to the Advanced Camera Techniques section in the Appendix. -==== - -In the next section, we'll explore how to use transformation matrices to position objects in our 3D scene. - -link:04_transformation_matrices.adoc[Next: Transformation Matrices] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc deleted file mode 100644 index cab14251..00000000 --- a/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc +++ /dev/null @@ -1,175 +0,0 @@ -:pp: {plus}{plus} - -= Camera & Transformations: Transformation Matrices - -== Transformation Matrices - -In this section, we'll dive deeper into the transformation matrices used in 3D graphics and how they're applied in our rendering pipeline. - -=== The Model-View-Projection (MVP) Pipeline - -The transformation of vertices from object space to screen space involves a series of matrix multiplications, commonly known as the MVP pipeline: - -[source,cpp] ----- -// The complete transformation pipeline -glm::mat4 MVP = projectionMatrix * viewMatrix * modelMatrix; ----- - -Let's explore each of these matrices in detail. - -=== Model Matrix - -The model matrix transforms vertices from object space to world space. It positions, rotates, and scales objects in the world. - -[source,cpp] ----- -glm::mat4 createModelMatrix( - const glm::vec3& position, - const glm::vec3& rotation, - const glm::vec3& scale -) { - // Start with identity matrix - glm::mat4 model = glm::mat4(1.0f); - - // Apply transformations in order: scale, rotate, translate - model = glm::translate(model, position); - - // Apply rotations around each axis - model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); - model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); - model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); - - // Apply scaling - model = glm::scale(model, scale); - - return model; -} ----- - -=== View Matrix - -The view matrix transforms vertices from world space to view space (camera space). It represents the position and orientation of the camera. - -[source,cpp] ----- -glm::mat4 createViewMatrix( - const glm::vec3& cameraPosition, - const glm::vec3& cameraTarget, - const glm::vec3& upVector -) { - return glm::lookAt(cameraPosition, cameraTarget, upVector); -} ----- - -The `lookAt` function creates a view matrix that positions the camera at `cameraPosition`, looking at `cameraTarget`, with `upVector` defining the up direction. - -=== Projection Matrix - -The projection matrix transforms vertices from view space to clip space. It defines how 3D coordinates are projected onto the 2D screen. - -==== Perspective Projection - -Perspective projection simulates how objects appear smaller as they get farther away, which is how our eyes naturally perceive the world. - -[source,cpp] ----- -glm::mat4 createPerspectiveMatrix( - float fovY, - float aspectRatio, - float nearPlane, - float farPlane -) { - return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); -} ----- - -Parameters: -* `fovY`: Field of view angle in degrees (vertical) -* `aspectRatio`: Width divided by height of the viewport -* `nearPlane`: Distance to the near clipping plane -* `farPlane`: Distance to the far clipping plane - -==== Orthographic Projection - -Orthographic projection doesn't have perspective distortion, making it useful for 2D rendering or technical drawings. - -[source,cpp] ----- -glm::mat4 createOrthographicMatrix( - float left, - float right, - float bottom, - float top, - float nearPlane, - float farPlane -) { - return glm::ortho(left, right, bottom, top, nearPlane, farPlane); -} ----- - -=== Normal Matrix - -When applying non-uniform scaling to objects, normals can become incorrect if transformed with the model matrix. The normal matrix solves this issue: - -[source,cpp] ----- -glm::mat3 createNormalMatrix(const glm::mat4& modelMatrix) { - // The normal matrix is the transpose of the inverse of the upper-left 3x3 part of the model matrix - return glm::transpose(glm::inverse(glm::mat3(modelMatrix))); -} ----- - -=== Applying Transformations in Shaders - -In Vulkan, we typically pass these matrices to our shaders as uniform variables: - -[source,glsl] ----- -// Vertex shader -#version 450 - -layout(binding = 0) uniform UniformBufferObject { - mat4 model; - mat4 view; - mat4 proj; -} ubo; - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec2 inTexCoord; - -layout(location = 0) out vec3 fragNormal; -layout(location = 1) out vec2 fragTexCoord; - -void main() { - // Apply MVP transformation - gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); - - // Transform normal using normal matrix - mat3 normalMatrix = transpose(inverse(mat3(ubo.model))); - fragNormal = normalMatrix * inNormal; - - fragTexCoord = inTexCoord; -} ----- - -=== Hierarchical Transformations - -For complex objects or scenes with parent-child relationships, we use hierarchical transformations: - -[source,cpp] ----- -// Parent transformation -glm::mat4 parentModel = createModelMatrix(parentPosition, parentRotation, parentScale); - -// Child transformation relative to parent -glm::mat4 localModel = createModelMatrix(childLocalPosition, childLocalRotation, childLocalScale); - -// Combined transformation -glm::mat4 childWorldModel = parentModel * localModel; ----- - -In the next section, we'll integrate our camera system and transformation matrices with Vulkan to render 3D scenes. - -link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc index fbc3369f..20d96305 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc @@ -8,7 +8,7 @@ This chapter covers the implementation of a 3D camera system and the mathematica * link:01_introduction.adoc[Introduction] * link:02_math_foundations.adoc[Mathematical Foundations] -* link:03_camera_implementation.adoc[Camera Implementation] -* link:04_transformation_matrices.adoc[Transformation Matrices] +* link:03_transformation_matrices.adoc[Transformation Matrices] +* link:04_camera_implementation.adoc[Camera Implementation] * link:05_vulkan_integration.adoc[Vulkan Integration] * link:06_conclusion.adoc[Conclusion] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc index 920183f4..8feb8151 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc @@ -289,6 +289,7 @@ private: // Core Vulkan GPU resources for texture representation vk::Image image; // GPU image object containing pixel data vk::DeviceMemory memory; // GPU memory allocation backing the image + vk::DeviceSize offset; // Offset within the memory allocation for this texture vk::ImageView imageView; // Shader-accessible view into the image vk::Sampler sampler; // Sampling configuration (filtering, wrapping, etc.) @@ -343,7 +344,7 @@ The temporary nature of the CPU-side image data reflects the typical texture loa === Texture Resource: GPU Resource Cleanup and Memory Management -Then, we implement comprehensive resource cleanup that ensures all GPU resources are properly released when the texture is no longer needed, preventing memory leaks in long-running applications. +Then, we implement comprehensive resource cleanup that ensures all GPU resources are properly released when the texture is no longer needed, preventing memory leaks in long-running applications. Please note that if you have vk::raii objects, the destructor of the vk::raii objects will automatically handle the cleanup of the GPU resources. If, however, you have a vk::Device object, you must manually destroy the GPU resources to prevent memory leaks. Additionally, you need to have initialized the defaultDispatcher for the vk::Device object types. In the event that you are using vk::Device objects, the Unload function below details explicit releasing of the objects. [source,cpp] ---- @@ -443,11 +444,13 @@ private: // Vertex data management - stores per-vertex attributes like position, normal, UV coordinates vk::Buffer vertexBuffer; // GPU buffer containing vertex attribute data vk::DeviceMemory vertexBufferMemory; // GPU memory backing the vertex buffer + vk::DeviceSize vertexBufferOffset; // Offset within the memory allocation for vertex buffer uint32_t vertexCount = 0; // Number of vertices in this mesh // Index data management - defines triangle connectivity using vertex indices vk::Buffer indexBuffer; // GPU buffer containing triangle index data vk::DeviceMemory indexBufferMemory; // GPU memory backing the index buffer + vk::DeviceSize indexBufferOffset; // Offset within the memory allocation for index buffer uint32_t indexCount = 0; // Number of indices in this mesh (typically 3 per triangle) public: @@ -499,9 +502,9 @@ The temporary CPU-side storage approach enables validation and processing of geo The metadata caching strategy stores frequently accessed information locally to avoid expensive GPU queries during rendering. These counts are essential for draw calls, where the GPU needs to know exactly how many vertices to process and how many triangles to render, making local storage much more efficient than querying the GPU buffers repeatedly. -=== Mesh Resource — Then: GPU Resource Cleanup and Memory Reclamation +=== Mesh Resource: GPU Resource Cleanup and Memory Reclamation -Then, we implement comprehensive cleanup that properly releases all GPU resources and memory allocations when the mesh is no longer needed, ensuring robust memory management in long-running applications. +Then, we implement comprehensive cleanup that properly releases all GPU resources and memory allocations when the mesh is no longer needed, ensuring robust memory management in long-running applications. As mentioned above, if you have vk::raii objects, the destructor of the vk::raii objects will automatically handle the cleanup of the GPU resources. If, however, you have a vk::Device object, you must manually destroy the GPU resources to prevent memory leaks. Additionally, you need to have initialized the defaultDispatcher for the vk::Device object types. In the event that you are using vk::Device objects, the Unload function below details explicit releasing of the objects. [source,cpp] ---- @@ -594,6 +597,13 @@ The buffer creation methods represent some of the most performance-critical code The device access pattern illustrates a common architectural challenge in resource management systems: balancing convenience with loose coupling. While direct access to global singletons can simplify implementation, production systems typically use dependency injection or service locator patterns to maintain testability and flexibility while providing access to core engine services. +=== Shader Resource Implementation + +The Shader resource represents the programmable stages of the graphics pipeline, managing compilation, loading, and runtime management of shader programs. +This implementation demonstrates how to handle SPIR-V shader modules while providing clean interfaces for shader stage management and hot reloading support during development. + +[source,cpp] +---- // Shader resource class Shader : public Resource { private: diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index 77715ee4..94f38ec2 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -105,7 +105,7 @@ public: Modern rendering techniques often require multiple passes, each with a specific purpose. A render pass manager helps organize these passes and their dependencies. -In this tutorial, we use Vulkan's dynamic rendering feature with vk::raii instead of traditional render passes. Dynamic rendering simplifies the rendering process by allowing us to begin and end rendering operations with a single command, without explicitly creating VkRenderPass and VkFramebuffer objects. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Additionally, our engine uses C++20 modules for better code organization, faster compilation times, and improved encapsulation. +In this tutorial, we use Vulkan's dynamic rendering feature with vk::raii instead of traditional render passes. Dynamic rendering simplifies the rendering process by allowing us to begin and end rendering operations with a single command, without explicitly creating VkRenderPass and VkFramebuffer objects. === Rendergraphs and Synchronization @@ -131,8 +131,8 @@ First, we need to establish the fundamental data structures that represent rend class Rendergraph { private: // Resource description and management structure - // Represents any GPU resource used during rendering (textures, render targets, buffers) - struct Resource { + // Represents Image resource used during rendering (textures) + struct ImageResource { std::string name; // Human-readable identifier for debugging and referencing vk::Format format; // Pixel format (RGBA8, Depth24Stencil8, etc.) vk::Extent2D extent; // Dimensions in pixels for 2D resources diff --git a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc index 3a3a7131..df4a451c 100644 --- a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc +++ b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc @@ -223,7 +223,7 @@ ImGuiVulkanUtil::ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDev // Set up dynamic rendering info renderingInfo.colorAttachmentCount = 1; vk::Format formats[] = { colorFormat }; - renderingInfo.pColorAttachmentFormats = formats; + renderingInfo.pColorAttachmentFormats = &colorFormat; } ImGuiVulkanUtil::~ImGuiVulkanUtil() { From ce7c00d44050faddbd0a7f0fc1172851405c8230 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 17 Oct 2025 10:37:51 -0700 Subject: [PATCH 102/102] This doesn't currently fix the bugs still in the engine, but it's a checkin so I can work on other things in the tutorial and come back to this. Integrate mikktspace tangent generation, enhance robust tangent handling for poles, unify shader refraction logic, improve transmission Fresnel, and implement thread-safe rigid body management. --- attachments/simple_engine/CMakeLists.txt | 6 +- attachments/simple_engine/audio_system.cpp | 1 + attachments/simple_engine/audio_system.h | 8 + attachments/simple_engine/engine.cpp | 68 +- attachments/simple_engine/imgui_system.cpp | 4 +- attachments/simple_engine/imgui_system.h | 8 + attachments/simple_engine/mesh_component.cpp | 14 +- attachments/simple_engine/mikktspace.c | 1899 +++++++++++++++++ attachments/simple_engine/mikktspace.h | 145 ++ attachments/simple_engine/model_loader.cpp | 293 ++- attachments/simple_engine/model_loader.h | 12 +- attachments/simple_engine/physics_system.cpp | 24 +- attachments/simple_engine/physics_system.h | 11 + attachments/simple_engine/pipeline.h | 1 + attachments/simple_engine/renderer.h | 52 +- attachments/simple_engine/renderer_core.cpp | 57 +- .../simple_engine/renderer_pipelines.cpp | 54 +- .../simple_engine/renderer_rendering.cpp | 868 ++++---- .../simple_engine/renderer_resources.cpp | 632 +++--- attachments/simple_engine/scene_loading.cpp | 23 +- attachments/simple_engine/scene_loading.h | 2 +- attachments/simple_engine/shaders/pbr.slang | 348 ++- attachments/simple_engine/thread_pool.h | 86 + attachments/simple_engine/vulkan_device.cpp | 13 +- 24 files changed, 3323 insertions(+), 1306 deletions(-) create mode 100644 attachments/simple_engine/mikktspace.c create mode 100644 attachments/simple_engine/mikktspace.h create mode 100644 attachments/simple_engine/thread_pool.h diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 488b61cc..4489b87e 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -44,11 +44,6 @@ target_sources(VulkanCppModule ) -# Add the vulkan.cppm file directly as a source file -target_sources(VulkanCppModule - PRIVATE - "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" -) # Platform-specific settings if(ANDROID) @@ -119,6 +114,7 @@ set(SOURCES pipeline.cpp descriptor_manager.cpp renderdoc_debug_system.cpp + mikktspace.c ) # Create executable diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 673b03dd..9cbbd56c 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // OpenAL headers #ifdef __APPLE__ diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index 4e5037c8..b07a8ad6 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -11,6 +11,7 @@ #include #include #include +#include /** * @brief Class representing an audio source. @@ -148,6 +149,13 @@ class AudioSystem { */ AudioSystem() = default; + // Constructor-based initialization to replace separate Initialize() calls + AudioSystem(Engine* engine, Renderer* renderer) { + if (!Initialize(engine, renderer)) { + throw std::runtime_error("AudioSystem: initialization failed"); + } + } + /** * @brief Flush audio output: clears pending processing and device buffers so playback restarts cleanly. */ diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 06113228..1075519d 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -13,11 +13,7 @@ // @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc Engine::Engine() - : resourceManager(std::make_unique()), - modelLoader(std::make_unique()), - audioSystem(std::make_unique()), - physicsSystem(std::make_unique()), - imguiSystem(std::make_unique()) { + : resourceManager(std::make_unique()) { } Engine::~Engine() { @@ -64,37 +60,25 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool return false; } - // Initialize model loader - if (!modelLoader->Initialize(renderer.get())) { - return false; - } - - // Connect model loader to renderer for light extraction - renderer->SetModelLoader(modelLoader.get()); - - // Initialize audio system - if (!audioSystem->Initialize(this, renderer.get())) { - return false; - } - - // Initialize a physics system - physicsSystem->SetRenderer(renderer.get()); + try { + // Model loader via constructor; also wire into renderer + modelLoader = std::make_unique(renderer.get()); + renderer->SetModelLoader(modelLoader.get()); - // Enable GPU acceleration for physics calculations to drastically speed up computations - physicsSystem->SetGPUAccelerationEnabled(true); + // Audio system via constructor + audioSystem = std::make_unique(this, renderer.get()); - if (!physicsSystem->Initialize()) { - return false; - } + // Physics system via constructor (GPU enabled) + physicsSystem = std::make_unique(renderer.get(), true); - // Initialize ImGui system - if (!imguiSystem->Initialize(renderer.get(), width, height)) { + // ImGui via constructor, then connect audio system + imguiSystem = std::make_unique(renderer.get(), width, height); + imguiSystem->SetAudioSystem(audioSystem.get()); + } catch (const std::exception& e) { + std::cerr << "Subsystem initialization failed: " << e.what() << std::endl; return false; } - // Connect ImGui system to an audio system for UI controls - imguiSystem->SetAudioSystem(audioSystem.get()); - // Generate ball material properties once at load time GenerateBallMaterial(); @@ -132,9 +116,15 @@ void Engine::Run() { // Update window title with FPS and frame time every second if (fpsUpdateTimer >= 1.0f) { uint64_t framesSinceLastUpdate = frameCount - lastFPSUpdateFrame; - currentFPS = framesSinceLastUpdate / fpsUpdateTimer; - // Average frame time in milliseconds over the last interval - double avgMs = (fpsUpdateTimer / static_cast(framesSinceLastUpdate)) * 1000.0; + double avgMs = 0.0; + if (framesSinceLastUpdate > 0 && fpsUpdateTimer > 0.0f) { + currentFPS = static_cast(static_cast(framesSinceLastUpdate) / static_cast(fpsUpdateTimer)); + avgMs = (fpsUpdateTimer / static_cast(framesSinceLastUpdate)) * 1000.0; + } else { + // Avoid divide-by-zero; keep previous FPS and estimate avgMs from last delta + currentFPS = std::max(currentFPS, 1.0f); + avgMs = static_cast(deltaTimeMs.count()); + } // Update window title with frame count, FPS, and frame time std::string title = "Simple Engine - Frame: " + std::to_string(frameCount) + @@ -402,15 +392,19 @@ void Engine::Update(TimeDelta deltaTime) { UpdateCameraControls(deltaTime); } - // Update all entities + // Update all entities (guard against null unique_ptrs) for (auto& entity : entities) { - if (entity->IsActive()) { - entity->Update(deltaTime); - } + if (!entity) { continue; } + if (!entity->IsActive()) { continue; } + entity->Update(deltaTime); } } void Engine::Render() { + // Ensure renderer is ready + if (!renderer || !renderer->IsInitialized()) { + return; + } // Check if we have an active camera if (!activeCamera) { diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index e3d17095..8cdbf325 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -218,7 +218,7 @@ void ImGuiSystem::NewFrame() { } // Exposure slider - static float exposure = 3.0f; // Higher default for emissive lighting + static float exposure = 1.2f; // Default tuned to avoid washout if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 10.0f, "%.2f")) { // Update exposure in renderer if (renderer) { @@ -228,7 +228,7 @@ void ImGuiSystem::NewFrame() { } ImGui::SameLine(); if (ImGui::Button("Reset##Exposure")) { - exposure = 3.0f; // Reset to higher value for emissive lighting + exposure = 1.2f; // Reset to a sane default to avoid washout if (renderer) { renderer->SetExposure(exposure); } diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index f9e2c237..d93e026f 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -5,6 +5,7 @@ #include #include #include +#include // Forward declarations class Renderer; @@ -25,6 +26,13 @@ class ImGuiSystem { */ ImGuiSystem(); + // Constructor-based initialization to replace separate Initialize() calls + ImGuiSystem(Renderer* renderer, uint32_t width, uint32_t height) { + if (!Initialize(renderer, width, height)) { + throw std::runtime_error("ImGuiSystem: initialization failed"); + } + } + /** * @brief Destructor for proper cleanup. */ diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index 6e2eef5d..05cc294c 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -36,13 +36,23 @@ void MeshComponent::CreateSphere(float radius, const glm::vec3& color, int segme static_cast(lat) / static_cast(segments) }; - // Calculate tangent (derivative with respect to longitude) + // Calculate tangent (derivative with respect to longitude). Handle poles robustly. glm::vec3 tangent = { -sinTheta * sinPhi, 0.0f, sinTheta * cosPhi }; - tangent = glm::normalize(tangent); + float len2 = glm::dot(tangent, tangent); + if (len2 < 1e-12f) { + // At poles sinTheta ~ 0 -> fallback tangent orthogonal to normal + glm::vec3 t = glm::cross(normal, glm::vec3(0.0f, 0.0f, 1.0f)); + if (glm::length(t) < 1e-12f) { + t = glm::cross(normal, glm::vec3(1.0f, 0.0f, 0.0f)); + } + tangent = glm::normalize(t); + } else { + tangent = glm::normalize(tangent); + } vertices.push_back({ position, diff --git a/attachments/simple_engine/mikktspace.c b/attachments/simple_engine/mikktspace.c new file mode 100644 index 00000000..0342ae01 --- /dev/null +++ b/attachments/simple_engine/mikktspace.c @@ -0,0 +1,1899 @@ +/** \file mikktspace/mikktspace.c + * \ingroup mikktspace + */ +/** + * Copyright (C) 2011 by Morten S. Mikkelsen + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include +#include +#include +#include +#include +#include + +#include "mikktspace.h" + +#define TFALSE 0 +#define TTRUE 1 + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define INTERNAL_RND_SORT_SEED 39871946 + +// internal structure +typedef struct { + float x, y, z; +} SVec3; + +static tbool veq( const SVec3 v1, const SVec3 v2 ) +{ + return (v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z); +} + +static SVec3 vadd( const SVec3 v1, const SVec3 v2 ) +{ + SVec3 vRes; + + vRes.x = v1.x + v2.x; + vRes.y = v1.y + v2.y; + vRes.z = v1.z + v2.z; + + return vRes; +} + + +static SVec3 vsub( const SVec3 v1, const SVec3 v2 ) +{ + SVec3 vRes; + + vRes.x = v1.x - v2.x; + vRes.y = v1.y - v2.y; + vRes.z = v1.z - v2.z; + + return vRes; +} + +static SVec3 vscale(const float fS, const SVec3 v) +{ + SVec3 vRes; + + vRes.x = fS * v.x; + vRes.y = fS * v.y; + vRes.z = fS * v.z; + + return vRes; +} + +static float LengthSquared( const SVec3 v ) +{ + return v.x*v.x + v.y*v.y + v.z*v.z; +} + +static float Length( const SVec3 v ) +{ + return sqrtf(LengthSquared(v)); +} + +static SVec3 Normalize( const SVec3 v ) +{ + return vscale(1 / Length(v), v); +} + +static float vdot( const SVec3 v1, const SVec3 v2) +{ + return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; +} + + +static tbool NotZero(const float fX) +{ + // could possibly use FLT_EPSILON instead + return fabsf(fX) > FLT_MIN; +} + +static tbool VNotZero(const SVec3 v) +{ + // might change this to an epsilon based test + return NotZero(v.x) || NotZero(v.y) || NotZero(v.z); +} + + + +typedef struct { + int iNrFaces; + int * pTriMembers; +} SSubGroup; + +typedef struct { + int iNrFaces; + int * pFaceIndices; + int iVertexRepresentitive; + tbool bOrientPreservering; +} SGroup; + +// +#define MARK_DEGENERATE 1 +#define QUAD_ONE_DEGEN_TRI 2 +#define GROUP_WITH_ANY 4 +#define ORIENT_PRESERVING 8 + + + +typedef struct { + int FaceNeighbors[3]; + SGroup * AssignedGroup[3]; + + // normalized first order face derivatives + SVec3 vOs, vOt; + float fMagS, fMagT; // original magnitudes + + // determines if the current and the next triangle are a quad. + int iOrgFaceNumber; + int iFlag, iTSpacesOffs; + unsigned char vert_num[4]; +} STriInfo; + +typedef struct { + SVec3 vOs; + float fMagS; + SVec3 vOt; + float fMagT; + int iCounter; // this is to average back into quads. + tbool bOrient; +} STSpace; + +static int GenerateInitialVerticesIndexList(STriInfo pTriInfos[], int piTriList_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn); +static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], + const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, + const SMikkTSpaceContext * pContext); + +static int MakeIndex(const int iFace, const int iVert) +{ + assert(iVert>=0 && iVert<4 && iFace>=0); + return (iFace<<2) | (iVert&0x3); +} + +static void IndexToData(int * piFace, int * piVert, const int iIndexIn) +{ + piVert[0] = iIndexIn&0x3; + piFace[0] = iIndexIn>>2; +} + +static STSpace AvgTSpace(const STSpace * pTS0, const STSpace * pTS1) +{ + STSpace ts_res; + + // this if is important. Due to floating point precision + // averaging when ts0==ts1 will cause a slight difference + // which results in tangent space splits later on + if (pTS0->fMagS==pTS1->fMagS && pTS0->fMagT==pTS1->fMagT && + veq(pTS0->vOs,pTS1->vOs) && veq(pTS0->vOt, pTS1->vOt)) + { + ts_res.fMagS = pTS0->fMagS; + ts_res.fMagT = pTS0->fMagT; + ts_res.vOs = pTS0->vOs; + ts_res.vOt = pTS0->vOt; + } + else + { + ts_res.fMagS = 0.5f*(pTS0->fMagS+pTS1->fMagS); + ts_res.fMagT = 0.5f*(pTS0->fMagT+pTS1->fMagT); + ts_res.vOs = vadd(pTS0->vOs,pTS1->vOs); + ts_res.vOt = vadd(pTS0->vOt,pTS1->vOt); + if ( VNotZero(ts_res.vOs) ) ts_res.vOs = Normalize(ts_res.vOs); + if ( VNotZero(ts_res.vOt) ) ts_res.vOt = Normalize(ts_res.vOt); + } + + return ts_res; +} + + + +static SVec3 GetPosition(const SMikkTSpaceContext * pContext, const int index); +static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index); +static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index); + + +// degen triangles +static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris); +static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris); + + +tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext) +{ + return genTangSpace(pContext, 180.0f); +} + +tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold) +{ + // count nr_triangles + int * piTriListIn = NULL, * piGroupTrianglesBuffer = NULL; + STriInfo * pTriInfos = NULL; + SGroup * pGroups = NULL; + STSpace * psTspace = NULL; + int iNrTrianglesIn = 0, f=0, t=0, i=0; + int iNrTSPaces = 0, iTotTris = 0, iDegenTriangles = 0, iNrMaxGroups = 0; + int iNrActiveGroups = 0, index = 0; + const int iNrFaces = pContext->m_pInterface->m_getNumFaces(pContext); + tbool bRes = TFALSE; + const float fThresCos = (float) cos((fAngularThreshold*(float)M_PI)/180.0f); + + // verify all call-backs have been set + if ( pContext->m_pInterface->m_getNumFaces==NULL || + pContext->m_pInterface->m_getNumVerticesOfFace==NULL || + pContext->m_pInterface->m_getPosition==NULL || + pContext->m_pInterface->m_getNormal==NULL || + pContext->m_pInterface->m_getTexCoord==NULL ) + return TFALSE; + + // count triangles on supported faces + for (f=0; fm_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts==3) ++iNrTrianglesIn; + else if (verts==4) iNrTrianglesIn += 2; + } + if (iNrTrianglesIn<=0) return TFALSE; + + // allocate memory for an index list + piTriListIn = (int *) malloc(sizeof(int)*3*iNrTrianglesIn); + pTriInfos = (STriInfo *) malloc(sizeof(STriInfo)*iNrTrianglesIn); + if (piTriListIn==NULL || pTriInfos==NULL) + { + if (piTriListIn!=NULL) free(piTriListIn); + if (pTriInfos!=NULL) free(pTriInfos); + return TFALSE; + } + + // make an initial triangle --> face index list + iNrTSPaces = GenerateInitialVerticesIndexList(pTriInfos, piTriListIn, pContext, iNrTrianglesIn); + + // make a welded index list of identical positions and attributes (pos, norm, texc) + //printf("gen welded index list begin\n"); + GenerateSharedVerticesIndexList(piTriListIn, pContext, iNrTrianglesIn); + //printf("gen welded index list end\n"); + + // Mark all degenerate triangles + iTotTris = iNrTrianglesIn; + iDegenTriangles = 0; + for (t=0; tm_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts!=3 && verts!=4) continue; + + + // I've decided to let degenerate triangles and group-with-anythings + // vary between left/right hand coordinate systems at the vertices. + // All healthy triangles on the other hand are built to always be either or. + + /*// force the coordinate system orientation to be uniform for every face. + // (this is already the case for good triangles but not for + // degenerate ones and those with bGroupWithAnything==true) + bool bOrient = psTspace[index].bOrient; + if (psTspace[index].iCounter == 0) // tspace was not derived from a group + { + // look for a space created in GenerateTSpaces() by iCounter>0 + bool bNotFound = true; + int i=1; + while (i 0) bNotFound=false; + else ++i; + } + if (!bNotFound) bOrient = psTspace[index+i].bOrient; + }*/ + + // set data + for (i=0; ivOs.x, pTSpace->vOs.y, pTSpace->vOs.z}; + float bitang[] = {pTSpace->vOt.x, pTSpace->vOt.y, pTSpace->vOt.z}; + if (pContext->m_pInterface->m_setTSpace!=NULL) + pContext->m_pInterface->m_setTSpace(pContext, tang, bitang, pTSpace->fMagS, pTSpace->fMagT, pTSpace->bOrient, f, i); + if (pContext->m_pInterface->m_setTSpaceBasic!=NULL) + pContext->m_pInterface->m_setTSpaceBasic(pContext, tang, pTSpace->bOrient==TTRUE ? 1.0f : (-1.0f), f, i); + + ++index; + } + } + + free(psTspace); + + + return TTRUE; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef struct { + float vert[3]; + int index; +} STmpVert; + +static const int g_iCells = 2048; + +#ifdef _MSC_VER +# define NOINLINE __declspec(noinline) +#else +# define NOINLINE __attribute__ ((noinline)) +#endif + +// it is IMPORTANT that this function is called to evaluate the hash since +// inlining could potentially reorder instructions and generate different +// results for the same effective input value fVal. +static NOINLINE int FindGridCell(const float fMin, const float fMax, const float fVal) +{ + const float fIndex = g_iCells * ((fVal-fMin)/(fMax-fMin)); + const int iIndex = (int)fIndex; + return iIndex < g_iCells ? (iIndex >= 0 ? iIndex : 0) : (g_iCells - 1); +} + +static void MergeVertsFast(int piTriList_in_and_out[], STmpVert pTmpVert[], const SMikkTSpaceContext * pContext, const int iL_in, const int iR_in); +static void MergeVertsSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int pTable[], const int iEntries); +static void GenerateSharedVerticesIndexListSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); + +static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) +{ + + // Generate bounding box + int * piHashTable=NULL, * piHashCount=NULL, * piHashOffsets=NULL, * piHashCount2=NULL; + STmpVert * pTmpVert = NULL; + int i=0, iChannel=0, k=0, e=0; + int iMaxCount=0; + SVec3 vMin = GetPosition(pContext, 0), vMax = vMin, vDim; + float fMin, fMax; + for (i=1; i<(iNrTrianglesIn*3); i++) + { + const int index = piTriList_in_and_out[i]; + + const SVec3 vP = GetPosition(pContext, index); + if (vMin.x > vP.x) vMin.x = vP.x; + else if (vMax.x < vP.x) vMax.x = vP.x; + if (vMin.y > vP.y) vMin.y = vP.y; + else if (vMax.y < vP.y) vMax.y = vP.y; + if (vMin.z > vP.z) vMin.z = vP.z; + else if (vMax.z < vP.z) vMax.z = vP.z; + } + + vDim = vsub(vMax,vMin); + iChannel = 0; + fMin = vMin.x; fMax=vMax.x; + if (vDim.y>vDim.x && vDim.y>vDim.z) + { + iChannel=1; + fMin = vMin.y; + fMax = vMax.y; + } + else if (vDim.z>vDim.x) + { + iChannel=2; + fMin = vMin.z; + fMax = vMax.z; + } + + // make allocations + piHashTable = (int *) malloc(sizeof(int)*iNrTrianglesIn*3); + piHashCount = (int *) malloc(sizeof(int)*g_iCells); + piHashOffsets = (int *) malloc(sizeof(int)*g_iCells); + piHashCount2 = (int *) malloc(sizeof(int)*g_iCells); + + if (piHashTable==NULL || piHashCount==NULL || piHashOffsets==NULL || piHashCount2==NULL) + { + if (piHashTable!=NULL) free(piHashTable); + if (piHashCount!=NULL) free(piHashCount); + if (piHashOffsets!=NULL) free(piHashOffsets); + if (piHashCount2!=NULL) free(piHashCount2); + GenerateSharedVerticesIndexListSlow(piTriList_in_and_out, pContext, iNrTrianglesIn); + return; + } + memset(piHashCount, 0, sizeof(int)*g_iCells); + memset(piHashCount2, 0, sizeof(int)*g_iCells); + + // count amount of elements in each cell unit + for (i=0; i<(iNrTrianglesIn*3); i++) + { + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition(pContext, index); + const float fVal = iChannel==0 ? vP.x : (iChannel==1 ? vP.y : vP.z); + const int iCell = FindGridCell(fMin, fMax, fVal); + ++piHashCount[iCell]; + } + + // evaluate start index of each cell. + piHashOffsets[0]=0; + for (k=1; kpTmpVert[l].vert[c]) fvMin[c]=pTmpVert[l].vert[c]; + if (fvMax[c]dx && dy>dz) channel=1; + else if (dz>dx) channel=2; + + fSep = 0.5f*(fvMax[channel]+fvMin[channel]); + + // stop if all vertices are NaNs + if (!isfinite(fSep)) + return; + + // terminate recursion when the separation/average value + // is no longer strictly between fMin and fMax values. + if (fSep>=fvMax[channel] || fSep<=fvMin[channel]) + { + // complete the weld + for (l=iL_in; l<=iR_in; l++) + { + int i = pTmpVert[l].index; + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition(pContext, index); + const SVec3 vN = GetNormal(pContext, index); + const SVec3 vT = GetTexCoord(pContext, index); + + tbool bNotFound = TTRUE; + int l2=iL_in, i2rec=-1; + while (l20); // at least 2 entries + + // separate (by fSep) all points between iL_in and iR_in in pTmpVert[] + while (iL < iR) + { + tbool bReadyLeftSwap = TFALSE, bReadyRightSwap = TFALSE; + while ((!bReadyLeftSwap) && iL=iL_in && iL<=iR_in); + bReadyLeftSwap = !(pTmpVert[iL].vert[channel]=iL_in && iR<=iR_in); + bReadyRightSwap = pTmpVert[iR].vert[channel]m_pInterface->m_getNumFaces(pContext); f++) + { + const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts!=3 && verts!=4) continue; + + pTriInfos[iDstTriIndex].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex].iTSpacesOffs = iTSpacesOffs; + + if (verts==3) + { + unsigned char * pVerts = pTriInfos[iDstTriIndex].vert_num; + pVerts[0]=0; pVerts[1]=1; pVerts[2]=2; + piTriList_out[iDstTriIndex*3+0] = MakeIndex(f, 0); + piTriList_out[iDstTriIndex*3+1] = MakeIndex(f, 1); + piTriList_out[iDstTriIndex*3+2] = MakeIndex(f, 2); + ++iDstTriIndex; // next + } + else + { + { + pTriInfos[iDstTriIndex+1].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex+1].iTSpacesOffs = iTSpacesOffs; + } + + { + // need an order independent way to evaluate + // tspace on quads. This is done by splitting + // along the shortest diagonal. + const int i0 = MakeIndex(f, 0); + const int i1 = MakeIndex(f, 1); + const int i2 = MakeIndex(f, 2); + const int i3 = MakeIndex(f, 3); + const SVec3 T0 = GetTexCoord(pContext, i0); + const SVec3 T1 = GetTexCoord(pContext, i1); + const SVec3 T2 = GetTexCoord(pContext, i2); + const SVec3 T3 = GetTexCoord(pContext, i3); + const float distSQ_02 = LengthSquared(vsub(T2,T0)); + const float distSQ_13 = LengthSquared(vsub(T3,T1)); + tbool bQuadDiagIs_02; + if (distSQ_02m_pInterface->m_getPosition(pContext, pos, iF, iI); + res.x=pos[0]; res.y=pos[1]; res.z=pos[2]; + return res; +} + +static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index) +{ + int iF, iI; + SVec3 res; float norm[3]; + IndexToData(&iF, &iI, index); + pContext->m_pInterface->m_getNormal(pContext, norm, iF, iI); + res.x=norm[0]; res.y=norm[1]; res.z=norm[2]; + return res; +} + +static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index) +{ + int iF, iI; + SVec3 res; float texc[2]; + IndexToData(&iF, &iI, index); + pContext->m_pInterface->m_getTexCoord(pContext, texc, iF, iI); + res.x=texc[0]; res.y=texc[1]; res.z=1.0f; + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef union { + struct + { + int i0, i1, f; + }; + int array[3]; +} SEdge; + +static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn); +static void BuildNeighborsSlow(STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn); + +// returns the texture area times 2 +static float CalcTexArea(const SMikkTSpaceContext * pContext, const int indices[]) +{ + const SVec3 t1 = GetTexCoord(pContext, indices[0]); + const SVec3 t2 = GetTexCoord(pContext, indices[1]); + const SVec3 t3 = GetTexCoord(pContext, indices[2]); + + const float t21x = t2.x-t1.x; + const float t21y = t2.y-t1.y; + const float t31x = t3.x-t1.x; + const float t31y = t3.y-t1.y; + + const float fSignedAreaSTx2 = t21x*t31y - t21y*t31x; + + return fSignedAreaSTx2<0 ? (-fSignedAreaSTx2) : fSignedAreaSTx2; +} + +static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) +{ + int f=0, i=0, t=0; + // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before this function. + + // generate neighbor info list + for (f=0; f0 ? ORIENT_PRESERVING : 0); + + if ( NotZero(fSignedAreaSTx2) ) + { + const float fAbsArea = fabsf(fSignedAreaSTx2); + const float fLenOs = Length(vOs); + const float fLenOt = Length(vOt); + const float fS = (pTriInfos[f].iFlag&ORIENT_PRESERVING)==0 ? (-1.0f) : 1.0f; + if ( NotZero(fLenOs) ) pTriInfos[f].vOs = vscale(fS/fLenOs, vOs); + if ( NotZero(fLenOt) ) pTriInfos[f].vOt = vscale(fS/fLenOt, vOt); + + // evaluate magnitudes prior to normalization of vOs and vOt + pTriInfos[f].fMagS = fLenOs / fAbsArea; + pTriInfos[f].fMagT = fLenOt / fAbsArea; + + // if this is a good triangle + if ( NotZero(pTriInfos[f].fMagS) && NotZero(pTriInfos[f].fMagT)) + pTriInfos[f].iFlag &= (~GROUP_WITH_ANY); + } + } + + // force otherwise healthy quads to a fixed orientation + while (t<(iNrTrianglesIn-1)) + { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; + if (iFO_a==iFO_b) // this is a quad + { + const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + + // bad triangles should already have been removed by + // DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false + if ((bIsDeg_a||bIsDeg_b)==TFALSE) + { + const tbool bOrientA = (pTriInfos[t].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bOrientB = (pTriInfos[t+1].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + // if this happens the quad has extremely bad mapping!! + if (bOrientA!=bOrientB) + { + //printf("found quad with bad mapping\n"); + tbool bChooseOrientFirstTri = TFALSE; + if ((pTriInfos[t+1].iFlag&GROUP_WITH_ANY)!=0) bChooseOrientFirstTri = TTRUE; + else if ( CalcTexArea(pContext, &piTriListIn[t*3+0]) >= CalcTexArea(pContext, &piTriListIn[(t+1)*3+0]) ) + bChooseOrientFirstTri = TTRUE; + + // force match + { + const int t0 = bChooseOrientFirstTri ? t : (t+1); + const int t1 = bChooseOrientFirstTri ? (t+1) : t; + pTriInfos[t1].iFlag &= (~ORIENT_PRESERVING); // clear first + pTriInfos[t1].iFlag |= (pTriInfos[t0].iFlag&ORIENT_PRESERVING); // copy bit + } + } + } + t += 2; + } + else + ++t; + } + + // match up edge pairs + { + SEdge * pEdges = (SEdge *) malloc(sizeof(SEdge)*iNrTrianglesIn*3); + if (pEdges==NULL) + BuildNeighborsSlow(pTriInfos, piTriListIn, iNrTrianglesIn); + else + { + BuildNeighborsFast(pTriInfos, pEdges, piTriListIn, iNrTrianglesIn); + + free(pEdges); + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], const int iMyTriIndex, SGroup * pGroup); +static void AddTriToGroup(SGroup * pGroup, const int iTriIndex); + +static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn) +{ + const int iNrMaxGroups = iNrTrianglesIn*3; + int iNrActiveGroups = 0; + int iOffset = 0, f=0, i=0; + (void)iNrMaxGroups; /* quiet warnings in non debug mode */ + for (f=0; fiVertexRepresentitive = vert_index; + pTriInfos[f].AssignedGroup[i]->bOrientPreservering = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0; + pTriInfos[f].AssignedGroup[i]->iNrFaces = 0; + pTriInfos[f].AssignedGroup[i]->pFaceIndices = &piGroupTrianglesBuffer[iOffset]; + ++iNrActiveGroups; + + AddTriToGroup(pTriInfos[f].AssignedGroup[i], f); + bOrPre = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + neigh_indexL = pTriInfos[f].FaceNeighbors[i]; + neigh_indexR = pTriInfos[f].FaceNeighbors[i>0?(i-1):2]; + if (neigh_indexL>=0) // neighbor + { + const tbool bAnswer = + AssignRecur(piTriListIn, pTriInfos, neigh_indexL, + pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = (pTriInfos[neigh_indexL].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; + assert(bAnswer || bDiff); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + if (neigh_indexR>=0) // neighbor + { + const tbool bAnswer = + AssignRecur(piTriListIn, pTriInfos, neigh_indexR, + pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = (pTriInfos[neigh_indexR].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; + assert(bAnswer || bDiff); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + + // update offset + iOffset += pTriInfos[f].AssignedGroup[i]->iNrFaces; + // since the groups are disjoint a triangle can never + // belong to more than 3 groups. Subsequently something + // is completely screwed if this assertion ever hits. + assert(iOffset <= iNrMaxGroups); + } + } + } + + return iNrActiveGroups; +} + +static void AddTriToGroup(SGroup * pGroup, const int iTriIndex) +{ + pGroup->pFaceIndices[pGroup->iNrFaces] = iTriIndex; + ++pGroup->iNrFaces; +} + +static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], + const int iMyTriIndex, SGroup * pGroup) +{ + STriInfo * pMyTriInfo = &psTriInfos[iMyTriIndex]; + + // track down vertex + const int iVertRep = pGroup->iVertexRepresentitive; + const int * pVerts = &piTriListIn[3*iMyTriIndex+0]; + int i=-1; + if (pVerts[0]==iVertRep) i=0; + else if (pVerts[1]==iVertRep) i=1; + else if (pVerts[2]==iVertRep) i=2; + assert(i>=0 && i<3); + + // early out + if (pMyTriInfo->AssignedGroup[i] == pGroup) return TTRUE; + else if (pMyTriInfo->AssignedGroup[i]!=NULL) return TFALSE; + if ((pMyTriInfo->iFlag&GROUP_WITH_ANY)!=0) + { + // first to group with a group-with-anything triangle + // determines it's orientation. + // This is the only existing order dependency in the code!! + if ( pMyTriInfo->AssignedGroup[0] == NULL && + pMyTriInfo->AssignedGroup[1] == NULL && + pMyTriInfo->AssignedGroup[2] == NULL ) + { + pMyTriInfo->iFlag &= (~ORIENT_PRESERVING); + pMyTriInfo->iFlag |= (pGroup->bOrientPreservering ? ORIENT_PRESERVING : 0); + } + } + { + const tbool bOrient = (pMyTriInfo->iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + if (bOrient != pGroup->bOrientPreservering) return TFALSE; + } + + AddTriToGroup(pGroup, iMyTriIndex); + pMyTriInfo->AssignedGroup[i] = pGroup; + + { + const int neigh_indexL = pMyTriInfo->FaceNeighbors[i]; + const int neigh_indexR = pMyTriInfo->FaceNeighbors[i>0?(i-1):2]; + if (neigh_indexL>=0) + AssignRecur(piTriListIn, psTriInfos, neigh_indexL, pGroup); + if (neigh_indexR>=0) + AssignRecur(piTriListIn, psTriInfos, neigh_indexR, pGroup); + } + + + + return TTRUE; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2); +static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed); +static STSpace EvalTspace(int face_indices[], const int iFaces, const int piTriListIn[], const STriInfo pTriInfos[], const SMikkTSpaceContext * pContext, const int iVertexRepresentitive); + +static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], + const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, + const SMikkTSpaceContext * pContext) +{ + STSpace * pSubGroupTspace = NULL; + SSubGroup * pUniSubGroups = NULL; + int * pTmpMembers = NULL; + int iMaxNrFaces=0, iUniqueTspaces=0, g=0, i=0; + for (g=0; giNrFaces; i++) // triangles + { + const int f = pGroup->pFaceIndices[i]; // triangle number + int index=-1, iVertIndex=-1, iOF_1=-1, iMembers=0, j=0, l=0; + SSubGroup tmp_group; + tbool bFound; + SVec3 n, vOs, vOt; + if (pTriInfos[f].AssignedGroup[0]==pGroup) index=0; + else if (pTriInfos[f].AssignedGroup[1]==pGroup) index=1; + else if (pTriInfos[f].AssignedGroup[2]==pGroup) index=2; + assert(index>=0 && index<3); + + iVertIndex = piTriListIn[f*3+index]; + assert(iVertIndex==pGroup->iVertexRepresentitive); + + // is normalized already + n = GetNormal(pContext, iVertIndex); + + // project + vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); + vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); + if ( VNotZero(vOs) ) vOs = Normalize(vOs); + if ( VNotZero(vOt) ) vOt = Normalize(vOt); + + // original face number + iOF_1 = pTriInfos[f].iOrgFaceNumber; + + iMembers = 0; + for (j=0; jiNrFaces; j++) + { + const int t = pGroup->pFaceIndices[j]; // triangle number + const int iOF_2 = pTriInfos[t].iOrgFaceNumber; + + // project + SVec3 vOs2 = vsub(pTriInfos[t].vOs, vscale(vdot(n,pTriInfos[t].vOs), n)); + SVec3 vOt2 = vsub(pTriInfos[t].vOt, vscale(vdot(n,pTriInfos[t].vOt), n)); + if ( VNotZero(vOs2) ) vOs2 = Normalize(vOs2); + if ( VNotZero(vOt2) ) vOt2 = Normalize(vOt2); + + { + const tbool bAny = ( (pTriInfos[f].iFlag | pTriInfos[t].iFlag) & GROUP_WITH_ANY )!=0 ? TTRUE : TFALSE; + // make sure triangles which belong to the same quad are joined. + const tbool bSameOrgFace = iOF_1==iOF_2 ? TTRUE : TFALSE; + + const float fCosS = vdot(vOs,vOs2); + const float fCosT = vdot(vOt,vOt2); + + assert(f!=t || bSameOrgFace); // sanity check + if (bAny || bSameOrgFace || (fCosS>fThresCos && fCosT>fThresCos)) + pTmpMembers[iMembers++] = t; + } + } + + // sort pTmpMembers + tmp_group.iNrFaces = iMembers; + tmp_group.pTriMembers = pTmpMembers; + if (iMembers>1) + { + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + QuickSort(pTmpMembers, 0, iMembers-1, uSeed); + } + + // look for an existing match + bFound = TFALSE; + l=0; + while (liVertexRepresentitive); + ++iUniqueSubGroups; + } + + // output tspace + { + const int iOffs = pTriInfos[f].iTSpacesOffs; + const int iVert = pTriInfos[f].vert_num[index]; + STSpace * pTS_out = &psTspace[iOffs+iVert]; + assert(pTS_out->iCounter<2); + assert(((pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0) == pGroup->bOrientPreservering); + if (pTS_out->iCounter==1) + { + *pTS_out = AvgTSpace(pTS_out, &pSubGroupTspace[l]); + pTS_out->iCounter = 2; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + else + { + assert(pTS_out->iCounter==0); + *pTS_out = pSubGroupTspace[l]; + pTS_out->iCounter = 1; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + } + } + + // clean up and offset iUniqueTspaces + for (s=0; s=0 && i<3); + + // project + index = piTriListIn[3*f+i]; + n = GetNormal(pContext, index); + vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); + vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); + if ( VNotZero(vOs) ) vOs = Normalize(vOs); + if ( VNotZero(vOt) ) vOt = Normalize(vOt); + + i2 = piTriListIn[3*f + (i<2?(i+1):0)]; + i1 = piTriListIn[3*f + i]; + i0 = piTriListIn[3*f + (i>0?(i-1):2)]; + + p0 = GetPosition(pContext, i0); + p1 = GetPosition(pContext, i1); + p2 = GetPosition(pContext, i2); + v1 = vsub(p0,p1); + v2 = vsub(p2,p1); + + // project + v1 = vsub(v1, vscale(vdot(n,v1),n)); if ( VNotZero(v1) ) v1 = Normalize(v1); + v2 = vsub(v2, vscale(vdot(n,v2),n)); if ( VNotZero(v2) ) v2 = Normalize(v2); + + // weight contribution by the angle + // between the two edge vectors + fCos = vdot(v1,v2); fCos=fCos>1?1:(fCos<(-1) ? (-1) : fCos); + fAngle = (float) acos(fCos); + fMagS = pTriInfos[f].fMagS; + fMagT = pTriInfos[f].fMagT; + + res.vOs=vadd(res.vOs, vscale(fAngle,vOs)); + res.vOt=vadd(res.vOt,vscale(fAngle,vOt)); + res.fMagS+=(fAngle*fMagS); + res.fMagT+=(fAngle*fMagT); + fAngleSum += fAngle; + } + } + + // normalize + if ( VNotZero(res.vOs) ) res.vOs = Normalize(res.vOs); + if ( VNotZero(res.vOt) ) res.vOt = Normalize(res.vOt); + if (fAngleSum>0) + { + res.fMagS /= fAngleSum; + res.fMagT /= fAngleSum; + } + + return res; +} + +static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2) +{ + tbool bStillSame=TTRUE; + int i=0; + if (pg1->iNrFaces!=pg2->iNrFaces) return TFALSE; + while (iiNrFaces && bStillSame) + { + bStillSame = pg1->pTriMembers[i]==pg2->pTriMembers[i] ? TTRUE : TFALSE; + if (bStillSame) ++i; + } + return bStillSame; +} + +static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed) +{ + int iL, iR, n, index, iMid, iTmp; + + // Random + unsigned int t=uSeed&31; + t=(uSeed<>(32-t)); + uSeed=uSeed+t+3; + // Random end + + iL=iLeft; iR=iRight; + n = (iR-iL)+1; + assert(n>=0); + index = (int) (uSeed%n); + + iMid=pSortBuffer[index + iL]; + + + do + { + while (pSortBuffer[iL] < iMid) + ++iL; + while (pSortBuffer[iR] > iMid) + --iR; + + if (iL <= iR) + { + iTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = iTmp; + ++iL; --iR; + } + } + while (iL <= iR); + + if (iLeft < iR) + QuickSort(pSortBuffer, iLeft, iR, uSeed); + if (iL < iRight) + QuickSort(pSortBuffer, iL, iRight, uSeed); +} + +///////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void QuickSortEdges(SEdge * pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed); +static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in); + +static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn) +{ + // build array of edges + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + int iEntries=0, iCurStartIndex=-1, f=0, i=0; + for (f=0; f pSortBuffer[iRight].array[channel]) + { + sTmp = pSortBuffer[iLeft]; + pSortBuffer[iLeft] = pSortBuffer[iRight]; + pSortBuffer[iRight] = sTmp; + } + return; + } + + // Random + t=uSeed&31; + t=(uSeed<>(32-t)); + uSeed=uSeed+t+3; + // Random end + + iL = iLeft; + iR = iRight; + n = (iR-iL)+1; + assert(n>=0); + index = (int) (uSeed%n); + + iMid=pSortBuffer[index + iL].array[channel]; + + do + { + while (pSortBuffer[iL].array[channel] < iMid) + ++iL; + while (pSortBuffer[iR].array[channel] > iMid) + --iR; + + if (iL <= iR) + { + sTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = sTmp; + ++iL; --iR; + } + } + while (iL <= iR); + + if (iLeft < iR) + QuickSortEdges(pSortBuffer, iLeft, iR, channel, uSeed); + if (iL < iRight) + QuickSortEdges(pSortBuffer, iL, iRight, channel, uSeed); +} + +// resolve ordering and edge number +static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in) +{ + *edgenum_out = -1; + + // test if first index is on the edge + if (indices[0]==i0_in || indices[0]==i1_in) + { + // test if second index is on the edge + if (indices[1]==i0_in || indices[1]==i1_in) + { + edgenum_out[0]=0; // first edge + i0_out[0]=indices[0]; + i1_out[0]=indices[1]; + } + else + { + edgenum_out[0]=2; // third edge + i0_out[0]=indices[2]; + i1_out[0]=indices[0]; + } + } + else + { + // only second and third index is on the edge + edgenum_out[0]=1; // second edge + i0_out[0]=indices[1]; + i1_out[0]=indices[2]; + } +} + + +///////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////// Degenerate triangles //////////////////////////////////// + +static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris) +{ + int iNextGoodTriangleSearchIndex=-1; + tbool bStillFindingGoodOnes; + + // locate quads with only one good triangle + int t=0; + while (t<(iTotTris-1)) + { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; + if (iFO_a==iFO_b) // this is a quad + { + const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + if ((bIsDeg_a^bIsDeg_b)!=0) + { + pTriInfos[t].iFlag |= QUAD_ONE_DEGEN_TRI; + pTriInfos[t+1].iFlag |= QUAD_ONE_DEGEN_TRI; + } + t += 2; + } + else + ++t; + } + + // reorder list so all degen triangles are moved to the back + // without reordering the good triangles + iNextGoodTriangleSearchIndex = 1; + t=0; + bStillFindingGoodOnes = TTRUE; + while (t (t+1)); + + // swap triangle t0 and t1 + if (!bJustADegenerate) + { + int i=0; + for (i=0; i<3; i++) + { + const int index = piTriList_out[t0*3+i]; + piTriList_out[t0*3+i] = piTriList_out[t1*3+i]; + piTriList_out[t1*3+i] = index; + } + { + const STriInfo tri_info = pTriInfos[t0]; + pTriInfos[t0] = pTriInfos[t1]; + pTriInfos[t1] = tri_info; + } + } + else + bStillFindingGoodOnes = TFALSE; // this is not supposed to happen + } + + if (bStillFindingGoodOnes) ++t; + } + + assert(bStillFindingGoodOnes); // code will still work. + assert(iNrTrianglesIn == t); +} + +static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris) +{ + int t=0, i=0; + // deal with degenerate triangles + // punishment for degenerate triangles is O(N^2) + for (t=iNrTrianglesIn; t http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf + * Note that though the tangent spaces at the vertices are generated in an order-independent way, + * by this implementation, the interpolated tangent space is still affected by which diagonal is + * chosen to split each quad. A sensible solution is to have your tools pipeline always + * split quads by the shortest diagonal. This choice is order-independent and works with mirroring. + * If these have the same length then compare the diagonals defined by the texture coordinates. + * XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin + * and also quad triangulator plugin. + */ + + +typedef int tbool; +typedef struct SMikkTSpaceContext SMikkTSpaceContext; + +typedef struct { + // Returns the number of faces (triangles/quads) on the mesh to be processed. + int (*m_getNumFaces)(const SMikkTSpaceContext * pContext); + + // Returns the number of vertices on face number iFace + // iFace is a number in the range {0, 1, ..., getNumFaces()-1} + int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext * pContext, const int iFace); + + // returns the position/normal/texcoord of the referenced face of vertex number iVert. + // iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads. + void (*m_getPosition)(const SMikkTSpaceContext * pContext, float fvPosOut[], const int iFace, const int iVert); + void (*m_getNormal)(const SMikkTSpaceContext * pContext, float fvNormOut[], const int iFace, const int iVert); + void (*m_getTexCoord)(const SMikkTSpaceContext * pContext, float fvTexcOut[], const int iFace, const int iVert); + + // either (or both) of the two setTSpace callbacks can be set. + // The call-back m_setTSpaceBasic() is sufficient for basic normal mapping. + + // This function is used to return the tangent and fSign to the application. + // fvTangent is a unit length vector. + // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. + // bitangent = fSign * cross(vN, tangent); + // Note that the results are returned unindexed. It is possible to generate a new index list + // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. + // DO NOT! use an already existing index list. + void (*m_setTSpaceBasic)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert); + + // This function is used to return tangent space results to the application. + // fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their + // true magnitudes which can be used for relief mapping effects. + // fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent. + // However, both are perpendicular to the vertex normal. + // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. + // fSign = bIsOrientationPreserving ? 1.0f : (-1.0f); + // bitangent = fSign * cross(vN, tangent); + // Note that the results are returned unindexed. It is possible to generate a new index list + // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. + // DO NOT! use an already existing index list. + void (*m_setTSpace)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT, + const tbool bIsOrientationPreserving, const int iFace, const int iVert); +} SMikkTSpaceInterface; + +struct SMikkTSpaceContext +{ + SMikkTSpaceInterface * m_pInterface; // initialized with callback functions + void * m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter with every interface call) +}; + +// these are both thread safe! +tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext); // Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled) +tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold); + + +// To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the +// normal map sampler must use the exact inverse of the pixel shader transformation. +// The most efficient transformation we can possibly do in the pixel shader is +// achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN. +// pixel shader (fast transform out) +// vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN ); +// where vNt is the tangent space normal. The normal map sampler must likewise use the +// interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader. +// sampler does (exact inverse of pixel shader): +// float3 row0 = cross(vB, vN); +// float3 row1 = cross(vN, vT); +// float3 row2 = cross(vT, vB); +// float fSign = dot(vT, row0)<0 ? -1 : 1; +// vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) ); +// where vNout is the sampled normal in some chosen 3D space. +// +// Should you choose to reconstruct the bitangent in the pixel shader instead +// of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also. +// Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of +// quads as your renderer then problems will occur since the interpolated tangent spaces will differ +// eventhough the vertex level tangent spaces match. This can be solved either by triangulating before +// sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier. +// However, this must be used both by the sampler and your tools/rendering pipeline. + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 90b09076..ab7f7eeb 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -2,11 +2,67 @@ #include "renderer.h" #include "mesh_component.h" #include -#include #include #include #include +#include "mikktspace.h" + +// This struct acts as a bridge between the C-style MikkTSpace callbacks +// and our C++ MaterialMesh vertex data. It's passed via the m_pUserData pointer. +struct MikkTSpaceInterface { + std::vector* vertices; + std::vector* indices; +}; + +// These static callback functions are required by the MikkTSpace library. +// They are defined here at file-scope so they are not part of the ModelLoader class. +static int getNumFaces(const SMikkTSpaceContext* pContext) { + auto* userData = static_cast(pContext->m_pUserData); + return static_cast(userData->indices->size() / 3); +} + +static int getNumVerticesOfFace(const SMikkTSpaceContext* pContext, const int iFace) { + return 3; +} + +static void getPosition(const SMikkTSpaceContext* pContext, float fvPosOut[], const int iFace, const int iVert) { + auto* userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + const glm::vec3& pos = (*userData->vertices)[index].position; + fvPosOut[0] = pos.x; + fvPosOut[1] = pos.y; + fvPosOut[2] = pos.z; +} + +static void getNormal(const SMikkTSpaceContext* pContext, float fvNormOut[], const int iFace, const int iVert) { + auto* userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + const glm::vec3& norm = (*userData->vertices)[index].normal; + fvNormOut[0] = norm.x; + fvNormOut[1] = norm.y; + fvNormOut[2] = norm.z; +} + +static void getTexCoord(const SMikkTSpaceContext* pContext, float fvTexcOut[], const int iFace, const int iVert) { + auto* userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + const glm::vec2& uv = (*userData->vertices)[index].texCoord; + fvTexcOut[0] = uv.x; + fvTexcOut[1] = uv.y; +} + +static void setTSpaceBasic(const SMikkTSpaceContext* pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert) { + auto* userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + Vertex& vert = (*userData->vertices)[index]; + vert.tangent.x = fvTangent[0]; + vert.tangent.y = fvTangent[1]; + vert.tangent.z = fvTangent[2]; + // Clamp handedness to +/-1 to avoid tiny floating deviations + vert.tangent.w = (fSign >= 0.0f) ? 1.0f : -1.0f; +} + // KTX2 decoding for GLTF images #include @@ -216,18 +272,19 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { gltfMaterial.emissiveFactor[1], gltfMaterial.emissiveFactor[2] ); + material->emissive *= light_scale; } // Parse KHR_materials_emissive_strength extension auto extensionIt = gltfMaterial.extensions.find("KHR_materials_emissive_strength"); if (extensionIt != gltfMaterial.extensions.end()) { + hasEmissiveStrengthExtension = true; const tinygltf::Value& extension = extensionIt->second; if (extension.Has("emissiveStrength") && extension.Get("emissiveStrength").IsNumber()) { material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()) * light_scale; } } else { - // Default emissive strength is 1.0, according to GLTF spec, scaled for engine units - material->emissiveStrength = 1.0f * light_scale; + material->emissiveStrength = 0.0f; } // Alpha mode / cutoff @@ -885,108 +942,152 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { MaterialMesh& materialMesh = geometryMaterialMeshMap[geometryHash]; // Only process geometry if this MaterialMesh is empty (first time processing this geometry) - if (materialMesh.vertices.empty()) { - - auto vertexOffsetInMaterialMesh = static_cast(materialMesh.vertices.size()); - - // Get indices for this primitive - if (primitive.indices >= 0) { - const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; - const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; - const tinygltf::Buffer& indexBuffer = gltfModel.buffers[indexBufferView.buffer]; - - const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; - - // Handle different index types with proper vertex offset adjustment - if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - const auto* buf = static_cast(indexData); - for (size_t i = 0; i < indexAccessor.count; ++i) { - // FIXED: Add vertex offset to prevent index sharing between primitives - materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); - } - } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - const auto* buf = static_cast(indexData); - for (size_t i = 0; i < indexAccessor.count; ++i) { - // FIXED: Add vertex offset to prevent index sharing between primitives - materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); - } - } +if (materialMesh.vertices.empty()) { + + auto vertexOffsetInMaterialMesh = static_cast(materialMesh.vertices.size()); + + // Get indices for this primitive (your existing code is correct) + if (primitive.indices >= 0) { + const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; + const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; + const tinygltf::Buffer& indexBuffer = gltfModel.buffers[indexBufferView.buffer]; + const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; + if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + const auto* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); } - - // Get vertex positions - auto posIt = primitive.attributes.find("POSITION"); - if (posIt == primitive.attributes.end()) { - std::cerr << "No POSITION attribute found in primitive" << std::endl; - continue; + } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + const auto* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); } + } + } - const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; - const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; - const tinygltf::Buffer& posBuffer = gltfModel.buffers[posBufferView.buffer]; - - const auto* positions = reinterpret_cast( - &posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset]); - - // Get texture coordinates (if available) - const float* texCoords = nullptr; - auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); - if (texCoordIt != primitive.attributes.end()) { - const tinygltf::Accessor& texCoordAccessor = gltfModel.accessors[texCoordIt->second]; - const tinygltf::BufferView& texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; - const tinygltf::Buffer& texCoordBuffer = gltfModel.buffers[texCoordBufferView.buffer]; - texCoords = reinterpret_cast( - &texCoordBuffer.data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]); - } + // --- START: FINAL SAFE AND CORRECT VERTEX LOADING --- + + // Get the position accessor, which defines the vertex count. + auto posIt = primitive.attributes.find("POSITION"); + if (posIt == primitive.attributes.end()) continue; + const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; + + // Get data pointers and strides for all available attributes ONCE before the loop. + const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; + const tinygltf::Buffer& buffer = gltfModel.buffers[posBufferView.buffer]; + const unsigned char* pPositions = &buffer.data[posBufferView.byteOffset + posAccessor.byteOffset]; + const size_t posByteStride = posBufferView.byteStride == 0 ? sizeof(glm::vec3) : posBufferView.byteStride; + + const unsigned char* pNormals = nullptr; + size_t normalByteStride = 0; + auto normalIt = primitive.attributes.find("NORMAL"); + if (normalIt != primitive.attributes.end()) { + const tinygltf::Accessor& normalAccessor = gltfModel.accessors[normalIt->second]; + const tinygltf::BufferView& normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; + pNormals = &gltfModel.buffers[normalBufferView.buffer].data[normalBufferView.byteOffset + normalAccessor.byteOffset]; + normalByteStride = normalBufferView.byteStride == 0 ? sizeof(glm::vec3) : normalBufferView.byteStride; + } - // Get normals (if available) - const float* normals = nullptr; - auto normalIt = primitive.attributes.find("NORMAL"); - if (normalIt != primitive.attributes.end()) { - const tinygltf::Accessor& normalAccessor = gltfModel.accessors[normalIt->second]; - const tinygltf::BufferView& normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; - const tinygltf::Buffer& normalBuffer = gltfModel.buffers[normalBufferView.buffer]; - normals = reinterpret_cast( - &normalBuffer.data[normalBufferView.byteOffset + normalAccessor.byteOffset]); - } + const unsigned char* pTexCoords = nullptr; + size_t texCoordByteStride = 0; + auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); + if (texCoordIt != primitive.attributes.end()) { + const tinygltf::Accessor& texCoordAccessor = gltfModel.accessors[texCoordIt->second]; + const tinygltf::BufferView& texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; + pTexCoords = &gltfModel.buffers[texCoordBufferView.buffer].data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]; + texCoordByteStride = texCoordBufferView.byteStride == 0 ? sizeof(glm::vec2) : texCoordBufferView.byteStride; + } - // Create vertices in their original coordinate system (no transformation applied here) - for (size_t i = 0; i < posAccessor.count; ++i) { - Vertex vertex{}; + const unsigned char* pTangents = nullptr; + size_t tangentByteStride = 0; + auto tangentIt = primitive.attributes.find("TANGENT"); + bool hasTangents = (tangentIt != primitive.attributes.end()); + if (hasTangents) { + const tinygltf::Accessor& tangentAccessor = gltfModel.accessors[tangentIt->second]; + const tinygltf::BufferView& tangentBufferView = gltfModel.bufferViews[tangentAccessor.bufferView]; + pTangents = &gltfModel.buffers[tangentBufferView.buffer].data[tangentBufferView.byteOffset + tangentAccessor.byteOffset]; + tangentByteStride = tangentBufferView.byteStride == 0 ? sizeof(glm::vec4) : tangentBufferView.byteStride; + } - // Position (keep in an original coordinate system) - vertex.position = glm::vec3( - positions[i * 3 + 0], - positions[i * 3 + 1], - positions[i * 3 + 2] - ); + // Append vertices for this primitive preserving prior vertices + size_t baseVertex = materialMesh.vertices.size(); + materialMesh.vertices.resize(baseVertex + posAccessor.count); - // Normal (keep in an original coordinate system) - if (normals) { - vertex.normal = glm::vec3( - normals[i * 3 + 0], - normals[i * 3 + 1], - normals[i * 3 + 2] - ); - } else { - vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); // Default forward normal - } + // Use a SINGLE, SAFE loop to load all vertex data. + for (size_t i = 0; i < posAccessor.count; ++i) { + Vertex& vertex = materialMesh.vertices[baseVertex + i]; - // Texture coordinates - if (texCoords) { - vertex.texCoord = glm::vec2( - texCoords[i * 2 + 0], - texCoords[i * 2 + 1] - ); - } else { - vertex.texCoord = glm::vec2(0.0f, 0.0f); - } + vertex.position = *reinterpret_cast(pPositions + i * posByteStride); - // Tangent (default right tangent for now, could be extracted from GLTF if available) - vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + if (pNormals) { + vertex.normal = *reinterpret_cast(pNormals + i * normalByteStride); + } else { + vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); + } + // Normalize normals to ensure consistent magnitude + if (glm::dot(vertex.normal, vertex.normal) > 0.0f) { + vertex.normal = glm::normalize(vertex.normal); + } else { + vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); + } - materialMesh.vertices.push_back(vertex); + if (pTexCoords) { + vertex.texCoord = *reinterpret_cast(pTexCoords + i * texCoordByteStride); + } else { + vertex.texCoord = glm::vec2(0.0f, 0.0f); + } + + if (hasTangents && pTangents) { + // Load glTF tangent and ensure it is normalized and orthogonal to the normal. + glm::vec4 t4 = *reinterpret_cast(pTangents + i * tangentByteStride); + glm::vec3 T = glm::vec3(t4); + // Normalize tangent and make it orthogonal to normal to avoid skewed TBN + if (glm::dot(T, T) > 0.0f) { + T = glm::normalize(T); + T = glm::normalize(T - vertex.normal * glm::dot(vertex.normal, T)); + } else { + T = glm::vec3(1.0f, 0.0f, 0.0f); } - } // End of isFirstTimeProcessing block + float w = (t4.w >= 0.0f) ? 1.0f : -1.0f; // clamp handedness to +/-1 + vertex.tangent = glm::vec4(T, w); + } else { + // No tangents in source: use a safe default tangent (T=+X, handedness=+1) + vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + } + } + + // AFTER the mesh is fully built, generate tangents via MikkTSpace ONLY if the source mesh lacks glTF tangents. + if (!hasTangents) { + if (pNormals && pTexCoords && !materialMesh.indices.empty()) { + MikkTSpaceInterface mikkInterface; + mikkInterface.vertices = &materialMesh.vertices; + mikkInterface.indices = &materialMesh.indices; + + SMikkTSpaceInterface sm_interface{}; + sm_interface.m_getNumFaces = getNumFaces; + sm_interface.m_getNumVerticesOfFace = getNumVerticesOfFace; + sm_interface.m_getPosition = getPosition; + sm_interface.m_getNormal = getNormal; + sm_interface.m_getTexCoord = getTexCoord; + sm_interface.m_setTSpaceBasic = setTSpaceBasic; + + SMikkTSpaceContext mikk_context{}; + mikk_context.m_pInterface = &sm_interface; + mikk_context.m_pUserData = &mikkInterface; + + if (genTangSpaceDefault(&mikk_context)) { + std::cout << " Generated tangents (MikkTSpace) for material: " << materialMesh.materialName << std::endl; + } else { + std::cerr << " Failed to generate tangents for material: " << materialMesh.materialName << std::endl; + } + } else { + std::cout << " Skipping tangent generation (missing normals, UVs, or indices) for material: " << materialMesh.materialName << std::endl; + } + } else { + std::cout << " Using glTF-provided tangents for material: " << materialMesh.materialName << std::endl; + } + // --- END: FINAL SAFE AND CORRECT VERTEX LOADING --- +} // Add all instances to this MaterialMesh (both new and existing geometry) for (const glm::mat4& instanceTransform : instances) { @@ -1380,7 +1481,7 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m emissiveLight.type = ExtractedLight::Type::Emissive; emissiveLight.position = worldCenter; emissiveLight.color = material->emissive; - emissiveLight.intensity = material->emissiveStrength; + emissiveLight.intensity = (hasEmissiveStrengthExtension)?material->emissiveStrength:1.0f; emissiveLight.range = 1.0f; // Default range for emissive lights emissiveLight.sourceMaterial = material->GetName(); emissiveLight.direction = worldNormal; @@ -1397,7 +1498,7 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m emissiveLight.type = ExtractedLight::Type::Emissive; emissiveLight.position = center; emissiveLight.color = material->emissive; - emissiveLight.intensity = material->emissiveStrength; + emissiveLight.intensity = hasEmissiveStrengthExtension?material->emissiveStrength:1.0f; emissiveLight.range = 1.0f; // Default range for emissive lights emissiveLight.sourceMaterial = material->GetName(); emissiveLight.direction = avgNormal; diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index 56787d9b..01ed8031 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -8,6 +8,7 @@ #include #include #include "mesh_component.h" +#include class Renderer; class Mesh; @@ -31,6 +32,7 @@ class Material { float roughness = 1.0f; float ao = 1.0f; glm::vec3 emissive = glm::vec3(0.0f); + float ior = 1.5f; // Index of refraction float emissiveStrength = 1.0f; // KHR_materials_emissive_strength extension float alpha = 1.0f; // Base color alpha (from MR baseColorFactor or SpecGloss diffuseFactor) float transmissionFactor = 0.0f; // KHR_materials_transmission: 0=opaque, 1=fully transmissive @@ -172,8 +174,6 @@ class Model { // Camera data access methods [[nodiscard]] const std::vector& GetCameras() const { return cameras; } -public: - // Public access to cameras for model loader std::vector cameras; private: @@ -192,6 +192,12 @@ class ModelLoader { * @brief Default constructor. */ ModelLoader() = default; + // Constructor-based initialization to replace separate Initialize() calls + explicit ModelLoader(Renderer* _renderer) { + if (!Initialize(_renderer)) { + throw std::runtime_error("ModelLoader: initialization failed"); + } + } /** * @brief Destructor for proper cleanup. @@ -260,6 +266,8 @@ class ModelLoader { // Material meshes per model std::unordered_map> materialMeshes; + bool hasEmissiveStrengthExtension = false; + float light_scale = 1.0f; /** diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index fd3481b3..0cc9579f 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -212,7 +212,13 @@ void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) { } for (const auto& pc : toCreate) { if (!pc.entity) continue; - if (rigidBodies.size() >= maxGPUObjects) break; // avoid oversubscription + + // Check size limit with proper locking (CreateRigidBody will acquire the lock again, but that's safe) + { + std::lock_guard lock(rigidBodiesMutex); + if (rigidBodies.size() >= maxGPUObjects) break; // avoid oversubscription + } + RigidBody* rb = CreateRigidBody(pc.entity, pc.shape, pc.mass); if (rb) { rb->SetKinematic(pc.kinematic); @@ -224,7 +230,13 @@ void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) { // GPU-ONLY physics - NO CPU fallback available // Check if GPU physics is properly initialized and available - if (initialized && gpuAccelerationEnabled && renderer && rigidBodies.size() <= maxGPUObjects) { + bool canUseGPUPhysics = false; + { + std::lock_guard lock(rigidBodiesMutex); + canUseGPUPhysics = (rigidBodies.size() <= maxGPUObjects); + } + + if (initialized && gpuAccelerationEnabled && renderer && canUseGPUPhysics) { // Debug: Log that we're using GPU physics static bool gpuPhysicsLogged = false; if (!gpuPhysicsLogged) { @@ -260,13 +272,16 @@ RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, // Create a new rigid body auto rigidBody = std::make_unique(entity, shape, mass); - // Store the rigid body + // Store the rigid body with thread-safe access + std::lock_guard lock(rigidBodiesMutex); rigidBodies.push_back(std::move(rigidBody)); return rigidBodies.back().get(); } bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { + std::lock_guard lock(rigidBodiesMutex); + // Find the rigid body in the vector auto it = std::ranges::find_if(rigidBodies, [rigidBody](const std::unique_ptr& rb) { @@ -304,6 +319,9 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, glm::vec3 closestHitNormal; Entity* closestHitEntity = nullptr; + // Protect access to rigidBodies vector during iteration + std::lock_guard lock(rigidBodiesMutex); + // Check each rigid body for intersection for (const auto& rigidBody : rigidBodies) { auto concreteRigidBody = dynamic_cast(rigidBody.get()); diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index 96e5bb0a..bb21429e 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -6,6 +6,7 @@ #include #include #include +#include class Entity; class Renderer; @@ -196,6 +197,15 @@ class PhysicsSystem { */ PhysicsSystem() = default; + // Constructor-based initialization replacing separate Initialize/Set* calls + explicit PhysicsSystem(Renderer* _renderer, bool enableGPU = true) { + SetRenderer(_renderer); + SetGPUAccelerationEnabled(enableGPU); + if (!Initialize()) { + throw std::runtime_error("PhysicsSystem: initialization failed"); + } + } + /** * @brief Destructor for proper cleanup. */ @@ -315,6 +325,7 @@ class PhysicsSystem { std::vector pendingCreations; // Rigid bodies + mutable std::mutex rigidBodiesMutex; // Protect concurrent access to rigidBodies std::vector> rigidBodies; // Gravity diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h index b4ad8f6d..a6da24e4 100644 --- a/attachments/simple_engine/pipeline.h +++ b/attachments/simple_engine/pipeline.h @@ -31,6 +31,7 @@ struct MaterialProperties { alignas(4) int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness alignas(4) float glossinessFactor; // SpecGloss glossiness scalar alignas(16) glm::vec3 specularFactor; // SpecGloss specular color factor + alignas(4) float ior; // Index of refraction for transmission }; /** diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 78c52f9e..7eb69f7b 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -75,14 +75,11 @@ struct UniformBufferObject { alignas(4) float gamma; alignas(4) float prefilteredCubeMipLevels; alignas(4) float scaleIBLAmbient; - alignas(4) int lightCount; // Number of active lights (dynamic) - alignas(4) int padding0; // Padding for alignment (shadows removed) - alignas(4) float padding1; // Padding for alignment - alignas(4) float padding2; // Padding for alignment - - // Additional padding to ensure the structure size is aligned to 64 bytes (device nonCoherentAtomSize) - // Adjusted padding to maintain 256 bytes total size - alignas(4) float padding3[2]; // Add remaining bytes to reach 256 bytes total + alignas(4) int lightCount; + alignas(4) int padding0; // match shader UBO layout + alignas(4) float padding1; // match shader UBO layout + alignas(4) float padding2; // match shader UBO layout + alignas(8) glm::vec2 screenDimensions; }; @@ -107,6 +104,8 @@ struct MaterialProperties { alignas(4) int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness alignas(4) float glossinessFactor; // SpecGloss glossiness scalar alignas(16) glm::vec3 specularFactor; // SpecGloss specular color factor + alignas(4) float ior = 1.5f; // index of refraction + alignas(4) bool hasEmissiveStrengthExtension; }; /** @@ -203,7 +202,7 @@ class Renderer { // Expose uploads timeline semaphore and last value for external waits vk::Semaphore GetUploadsTimelineSemaphore() const { return *uploadsTimeline; } - uint64_t GetUploadsTimelineValue() const { return uploadTimelineValue.load(std::memory_order_relaxed); } + uint64_t GetUploadsTimelineValue() const { return uploadTimelineLastSubmitted.load(std::memory_order_relaxed); } /** * @brief Get the compute queue. @@ -396,7 +395,7 @@ class Renderer { * This should be called when the window is resized to trigger swap chain recreation. */ void SetFramebufferResized() { - framebufferResized = true; + framebufferResized.store(true, std::memory_order_relaxed); } /** @@ -490,7 +489,7 @@ class Renderer { // PBR rendering parameters float gamma = 2.2f; // Gamma correction value - float exposure = 3.0f; // HDR exposure value (higher for emissive lighting) + float exposure = 1.2f; // HDR exposure value (default tuned to avoid washout) // Sun control (UI-driven) float sunPosition = 0.5f; // 0..1, extremes are night, 0.5 is noon @@ -570,7 +569,8 @@ class Renderer { // Upload timeline semaphore for transfer -> graphics handoff (signaled per upload) vk::raii::Semaphore uploadsTimeline = nullptr; - std::atomic uploadTimelineValue{0}; + // Tracks last timeline value that has been submitted for signaling on uploadsTimeline + std::atomic uploadTimelineLastSubmitted{0}; // Depth buffer vk::raii::Image depthImage = nullptr; @@ -580,6 +580,20 @@ class Renderer { // Descriptor set layouts (declared before pools and sets) vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; + vk::raii::DescriptorSetLayout transparentDescriptorSetLayout = nullptr; + vk::raii::PipelineLayout pbrTransparentPipelineLayout = nullptr; + + // The texture that will hold a snapshot of the opaque scene + vk::raii::Image opaqueSceneColorImage{nullptr}; + vk::raii::DeviceMemory opaqueSceneColorImageMemory{nullptr}; // <-- Standard Vulkan memory + vk::raii::ImageView opaqueSceneColorImageView{nullptr}; + vk::raii::Sampler opaqueSceneColorSampler{nullptr}; + + // A descriptor set for the opaque scene color texture. We will have one for each frame in flight + // to match the swapchain images. + std::vector transparentDescriptorSets; + // Fallback descriptor sets for opaque pass (binds a default SHADER_READ_ONLY texture as Set 1) + std::vector transparentFallbackDescriptorSets; // Mesh resources struct MeshResources { @@ -599,6 +613,8 @@ class Renderer { vk::raii::Sampler textureSampler = nullptr; vk::Format format = vk::Format::eR8G8B8A8Srgb; // Store texture format for proper color space handling uint32_t mipLevels = 1; // Store number of mipmap levels + // Hint: true if source texture appears to use alpha masking (any alpha < ~1.0) + bool alphaMaskedHint = false; }; std::unordered_map textureResources; // Protect concurrent access to textureResources @@ -617,6 +633,8 @@ class Renderer { // Thread pool for background background tasks (textures, etc.) std::unique_ptr threadPool; + // Mutex to protect threadPool access during initialization/cleanup + mutable std::shared_mutex threadPoolMutex; // Texture loading progress (for UI) std::atomic textureTasksScheduled{0}; @@ -680,7 +698,8 @@ class Renderer { const std::vector optionalDeviceExtensions = { VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, - VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME + VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, + VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME }; // All device extensions (required + optional) @@ -689,8 +708,8 @@ class Renderer { // Initialization flag bool initialized = false; - // Framebuffer resized flag - bool framebufferResized = false; + // Framebuffer resized flag (atomic to handle platform callback vs. render thread) + std::atomic framebufferResized{false}; // Maximum number of frames in flight const uint32_t MAX_FRAMES_IN_FLIGHT = 2u; @@ -755,6 +774,9 @@ class Renderer { uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); + bool createOpaqueSceneColorResources(); + void createTransparentDescriptorSets(); + void createTransparentFallbackDescriptorSets(); std::pair> createBufferPooled(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 48aaffeb..2437c015 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -151,12 +151,26 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + if (!createOrResizeLightStorageBuffers(1)) { + std::cerr << "Failed to create initial light storage buffers" << std::endl; + return false; + } + + if (!createOpaqueSceneColorResources()) { + return false; + } + + createTransparentDescriptorSets(); + // Create default texture resources if (!createDefaultTextureResources()) { std::cerr << "Failed to create default texture resources" << std::endl; return false; } + // Create fallback transparent descriptor sets (must occur after default textures exist) + createTransparentFallbackDescriptorSets(); + // Create shared default PBR textures (to avoid creating hundreds of identical textures) if (!createSharedDefaultPBRTextures()) { std::cerr << "Failed to create shared default PBR textures" << std::endl; @@ -213,8 +227,11 @@ void Renderer::ensureThreadLocalVulkanInit() const { // Clean up renderer resources void Renderer::Cleanup() { // Ensure background workers are stopped before tearing down Vulkan resources - if (threadPool) { - threadPool.reset(); + { + std::unique_lock lock(threadPoolMutex); + if (threadPool) { + threadPool.reset(); + } } if (initialized) { std::cout << "Starting renderer cleanup..." << std::endl; @@ -229,6 +246,11 @@ void Renderer::Cleanup() { resources.uniformBufferAllocations.clear(); resources.uniformBuffersMapped.clear(); } + // Also clear global descriptor sets that are allocated from descriptorPool, so they are + // destroyed while the pool is still valid (avoid vkFreeDescriptorSets invalid pool errors) + transparentDescriptorSets.clear(); + transparentFallbackDescriptorSets.clear(); + computeDescriptorSets.clear(); std::cout << "Renderer cleanup completed." << std::endl; initialized = false; } @@ -462,14 +484,30 @@ void Renderer::addSupportedOptionalExtensions() { // Get available extensions auto availableExtensions = physicalDevice.enumerateDeviceExtensionProperties(); - // Check which optional extensions are supported and add them to deviceExtensions + // Build a set of available extension names for quick lookup + std::set avail; + for (const auto& e : availableExtensions) { avail.insert(e.extensionName); } + + // First, handle dependency: VK_EXT_attachment_feedback_loop_dynamic_state requires VK_EXT_attachment_feedback_loop_layout + const char* dynState = VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME; + const char* layoutReq = "VK_EXT_attachment_feedback_loop_layout"; + bool dynSupported = avail.count(dynState) > 0; + bool layoutSupported = avail.count(layoutReq) > 0; for (const auto& optionalExt : optionalDeviceExtensions) { - for (const auto& availableExt : availableExtensions) { - if (strcmp(availableExt.extensionName, optionalExt) == 0) { - deviceExtensions.push_back(optionalExt); - std::cout << "Adding optional extension: " << optionalExt << std::endl; - break; + if (std::strcmp(optionalExt, dynState) == 0) { + if (dynSupported && layoutSupported) { + deviceExtensions.push_back(dynState); + deviceExtensions.push_back(layoutReq); + std::cout << "Adding optional extension: " << dynState << std::endl; + std::cout << "Adding required-by-optional extension: " << layoutReq << std::endl; + } else if (dynSupported && !layoutSupported) { + std::cout << "Skipping extension due to missing dependency: " << dynState << " requires " << layoutReq << std::endl; } + continue; // handled + } + if (avail.count(optionalExt)) { + deviceExtensions.push_back(optionalExt); + std::cout << "Adding optional extension: " << optionalExt << std::endl; } } } catch (const std::exception& e) { @@ -502,6 +540,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { // Enable required features auto features = physicalDevice.getFeatures2(); features.features.samplerAnisotropy = vk::True; + features.features.depthBiasClamp = vk::True; // Explicitly configure device features to prevent validation layer warnings // These features are required by extensions or other features, so we enable them explicitly @@ -569,7 +608,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { }; vk::SemaphoreCreateInfo timelineCreateInfo{ .pNext = &typeInfo }; uploadsTimeline = vk::raii::Semaphore(device, timelineCreateInfo); - uploadTimelineValue.store(0, std::memory_order_relaxed); + uploadTimelineLastSubmitted.store(0, std::memory_order_relaxed); return true; } catch (const std::exception& e) { diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index 7924d372..c5910aa6 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -113,6 +113,14 @@ bool Renderer::createPBRDescriptorSetLayout() { pbrDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + // Binding 7: transparent passes input + // Layout for Set 1: Just the scene color texture + vk::DescriptorSetLayoutBinding sceneColorBinding{ + .binding = 0, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment + }; + vk::DescriptorSetLayoutCreateInfo transparentLayoutInfo{ .bindingCount = 1, .pBindings = &sceneColorBinding }; + transparentDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, transparentLayoutInfo); + return true; } catch (const std::exception& e) { std::cerr << "Failed to create PBR descriptor set layout: " << e.what() << std::endl; @@ -187,7 +195,7 @@ bool Renderer::createGraphicsPipeline() { .depthClampEnable = VK_FALSE, .rasterizerDiscardEnable = VK_FALSE, .polygonMode = vk::PolygonMode::eFill, - .cullMode = vk::CullModeFlagBits::eBack, + .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = VK_FALSE, .lineWidth = 1.0f @@ -258,6 +266,9 @@ bool Renderer::createGraphicsPipeline() { }; // Create the graphics pipeline + vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; + rasterizerBack.cullMode = vk::CullModeFlagBits::eBack; + vk::GraphicsPipelineCreateInfo pipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &mainPipelineRenderingCreateInfo, @@ -267,7 +278,7 @@ bool Renderer::createGraphicsPipeline() { .pVertexInputState = &vertexInputInfo, .pInputAssemblyState = &inputAssembly, .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, + .pRasterizationState = &rasterizerBack, .pMultisampleState = &multisampling, .pDepthStencilState = &depthStencil, .pColorBlendState = &colorBlending, @@ -359,7 +370,7 @@ bool Renderer::createPBRPipeline() { .depthClampEnable = VK_FALSE, .rasterizerDiscardEnable = VK_FALSE, .polygonMode = vk::PolygonMode::eFill, - .cullMode = vk::CullModeFlagBits::eBack, + .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = VK_FALSE, .lineWidth = 1.0f @@ -412,16 +423,23 @@ bool Renderer::createPBRPipeline() { .size = sizeof(MaterialProperties) }; - // Create a pipeline layout using the PBR descriptor set layout + std::array transparentSetLayouts = {*pbrDescriptorSetLayout, *transparentDescriptorSetLayout}; + // Create a pipeline layout for opaque PBR with only the PBR descriptor set (set 0) + std::array pbrOnlySetLayouts = {*pbrDescriptorSetLayout}; + // Create BOTH pipeline layouts with two descriptor sets (PBR set 0 + scene color set 1) vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ - .setLayoutCount = 1, - .pSetLayouts = &*pbrDescriptorSetLayout, + .setLayoutCount = static_cast(transparentSetLayouts.size()), + .pSetLayouts = transparentSetLayouts.data(), .pushConstantRangeCount = 1, .pPushConstantRanges = &pushConstantRange }; pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + // Transparent PBR layout uses the same two-set layout + vk::PipelineLayoutCreateInfo transparentPipelineLayoutInfo{ .setLayoutCount = static_cast(transparentSetLayouts.size()), .pSetLayouts = transparentSetLayouts.data(), .pushConstantRangeCount = 1, .pPushConstantRanges = &pushConstantRange }; + pbrTransparentPipelineLayout = vk::raii::PipelineLayout(device, transparentPipelineLayoutInfo); + // Create pipeline rendering info vk::Format depthFormat = findDepthFormat(); @@ -447,6 +465,9 @@ bool Renderer::createPBRPipeline() { vk::PipelineDepthStencilStateCreateInfo depthStencilOpaque = depthStencil; depthStencilOpaque.depthWriteEnable = VK_TRUE; + vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; + rasterizerBack.cullMode = vk::CullModeFlagBits::eBack; + vk::GraphicsPipelineCreateInfo opaquePipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &pbrPipelineRenderingCreateInfo, @@ -456,7 +477,7 @@ bool Renderer::createPBRPipeline() { .pVertexInputState = &vertexInputInfo, .pInputAssemblyState = &inputAssembly, .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, + .pRasterizationState = &rasterizerBack, .pMultisampleState = &multisampling, .pDepthStencilState = &depthStencilOpaque, .pColorBlendState = &colorBlendingOpaque, @@ -474,18 +495,12 @@ bool Renderer::createPBRPipeline() { blendedAttachment.blendEnable = VK_TRUE; blendedAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; blendedAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; - blendedAttachment.colorBlendOp = vk::BlendOp::eAdd; blendedAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; blendedAttachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; - blendedAttachment.alphaBlendOp = vk::BlendOp::eAdd; - vk::PipelineColorBlendStateCreateInfo colorBlendingBlended{ - .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eCopy, - .attachmentCount = 1, - .pAttachments = &blendedAttachment - }; + vk::PipelineColorBlendStateCreateInfo colorBlendingBlended{ .attachmentCount = 1, .pAttachments = &blendedAttachment }; vk::PipelineDepthStencilStateCreateInfo depthStencilBlended = depthStencil; depthStencilBlended.depthWriteEnable = VK_FALSE; + depthStencilBlended.depthCompareOp = vk::CompareOp::eLessOrEqual; vk::GraphicsPipelineCreateInfo blendedPipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, @@ -501,7 +516,7 @@ bool Renderer::createPBRPipeline() { .pDepthStencilState = &depthStencilBlended, .pColorBlendState = &colorBlendingBlended, .pDynamicState = &dynamicState, - .layout = *pbrPipelineLayout, + .layout = *pbrTransparentPipelineLayout, .renderPass = nullptr, .subpass = 0, .basePipelineHandle = nullptr, @@ -568,7 +583,7 @@ bool Renderer::createLightingPipeline() { .depthClampEnable = VK_FALSE, .rasterizerDiscardEnable = VK_FALSE, .polygonMode = vk::PolygonMode::eFill, - .cullMode = vk::CullModeFlagBits::eBack, + .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = VK_FALSE, .lineWidth = 1.0f @@ -651,6 +666,9 @@ bool Renderer::createLightingPipeline() { }; // Create a graphics pipeline + vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; + rasterizerBack.cullMode = vk::CullModeFlagBits::eBack; + vk::GraphicsPipelineCreateInfo pipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &lightingPipelineRenderingCreateInfo, @@ -660,7 +678,7 @@ bool Renderer::createLightingPipeline() { .pVertexInputState = &vertexInputInfo, .pInputAssemblyState = &inputAssembly, .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, + .pRasterizationState = &rasterizerBack, .pMultisampleState = &multisampling, .pDepthStencilState = &depthStencil, .pColorBlendState = &colorBlending, diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 621c8c81..fe68765f 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -38,7 +38,7 @@ bool Renderer::createSwapChain() { .imageColorSpace = surfaceFormat.colorSpace, .imageExtent = extent, .imageArrayLayers = 1, - .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst, .preTransform = swapChainSupport.capabilities.currentTransform, .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, .presentMode = presentMode, @@ -81,6 +81,9 @@ bool Renderer::createSwapChain() { // Create image views bool Renderer::createImageViews() { try { + opaqueSceneColorImage.clear(); + opaqueSceneColorImageView.clear(); + opaqueSceneColorSampler.clear(); // Resize image views vector swapChainImageViews.clear(); swapChainImageViews.reserve(swapChainImages.size()); @@ -253,8 +256,8 @@ void Renderer::cleanupSwapChain() { // Clean up swap chain image views swapChainImageViews.clear(); - // Clean up descriptor pool (this will automatically clean up descriptor sets) - descriptorPool = nullptr; + // Note: Keep descriptor pool alive here to ensure descriptor sets remain valid during swapchain recreation. + // descriptorPool is preserved; it will be managed during full renderer teardown. // Clean up pipelines graphicsPipeline = nullptr; @@ -295,6 +298,7 @@ void Renderer::recreateSwapChain() { // Recreate swap chain and related resources createSwapChain(); createImageViews(); + setupDynamicRendering(); createDepthResources(); // Recreate sync objects with correct sizing for new swap chain @@ -303,6 +307,11 @@ void Renderer::recreateSwapChain() { // Recreate descriptor pool and pipelines createDescriptorPool(); + // Recreate off-screen opaque scene color and descriptor sets needed by transparent pass + createOpaqueSceneColorResources(); + createTransparentDescriptorSets(); + createTransparentFallbackDescriptorSets(); + // Wait for all command buffers to complete before clearing resources for (const auto& fence : inFlightFences) { if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} @@ -318,6 +327,11 @@ void Renderer::recreateSwapChain() { createPBRPipeline(); createLightingPipeline(); + // Re-create command buffers to ensure fresh recording against new swapchain state + commandBuffers.clear(); + createCommandBuffers(); + currentFrame = 0; + // Recreate descriptor sets for all entities after swapchain/pipeline rebuild for (auto& [entity, resources] : entityResources) { if (!entity) continue; @@ -496,10 +510,17 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); // Set PBR parameters (use member variables for UI control) - ubo.exposure = this->exposure; + // Clamp exposure to a sane range to avoid washout + ubo.exposure = std::clamp(this->exposure, 0.2f, 2.0f); ubo.gamma = this->gamma; ubo.prefilteredCubeMipLevels = 0.0f; - ubo.scaleIBLAmbient = 0.5f; + ubo.scaleIBLAmbient = 0.25f; + ubo.screenDimensions = glm::vec2(swapChainExtent.width, swapChainExtent.height); + + // Signal to the shader whether swapchain is sRGB (1) or not (0) using padding0 + int outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || + swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? 1 : 0; + ubo.padding0 = outputIsSRGB; // Copy to uniform buffer std::memcpy(entityIt->second.uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); @@ -507,586 +528,421 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity // Render the scene void Renderer::Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { - // Mark rendering as active (informational flag for systems that care) - if (memoryPool) { - memoryPool->setRenderingActive(true); - } + if (memoryPool) memoryPool->setRenderingActive(true); + struct RenderingStateGuard { MemoryPool* pool; explicit RenderingStateGuard(MemoryPool* p) : pool(p) {} ~RenderingStateGuard() { if (pool) pool->setRenderingActive(false); } } guard(memoryPool.get()); - // Use RAII to ensure rendering state is always reset, even if an exception occurs - struct RenderingStateGuard { - MemoryPool* pool; - explicit RenderingStateGuard(MemoryPool* p) : pool(p) {} - ~RenderingStateGuard() { - if (pool) { - pool->setRenderingActive(false); - } - } - } guard(memoryPool.get()); - - // Wait for the previous frame to finish if (device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} - // Acquire the next image from the swap chain uint32_t imageIndex; - // Use currentFrame for consistent semaphore indexing throughout acquire/submit/present chain auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); imageIndex = result.second; - // Check if the swap chain needs to be recreated - if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { - framebufferResized = false; - - // If ImGui has started a frame, we need to end it properly before returning - if (imguiSystem) { - ImGui::EndFrame(); - } - + if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { + framebufferResized.store(false, std::memory_order_relaxed); + if (imguiSystem) ImGui::EndFrame(); recreateSwapChain(); return; } - if (result.first != vk::Result::eSuccess) { - throw std::runtime_error("Failed to acquire swap chain image"); - } + if (result.first != vk::Result::eSuccess) { throw std::runtime_error("Failed to acquire swap chain image"); } - // Reset the fence for the current frame device.resetFences(*inFlightFences[currentFrame]); + if (framebufferResized.load(std::memory_order_relaxed)) { recreateSwapChain(); return; } - // Reset the command buffer commandBuffers[currentFrame].reset(); - - // Record the command buffer commandBuffers[currentFrame].begin(vk::CommandBufferBeginInfo()); + if (framebufferResized.load(std::memory_order_relaxed)) { commandBuffers[currentFrame].end(); recreateSwapChain(); return; } - // Update dynamic rendering attachments - colorAttachments[0].setImageView(*swapChainImageViews[imageIndex]); - depthAttachment.setImageView(*depthImageView); - - // Update rendering area - renderingInfo.setRenderArea(vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); - - // Transition swapchain image layout for rendering - vk::ImageMemoryBarrier renderBarrier{ - .srcAccessMask = vk::AccessFlagBits::eNone, - .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, - .oldLayout = vk::ImageLayout::eUndefined, - .newLayout = vk::ImageLayout::eColorAttachmentOptimal, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = swapChainImages[imageIndex], - .subresourceRange = { - .aspectMask = vk::ImageAspectFlagBits::eColor, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 - } - }; - - commandBuffers[currentFrame].pipelineBarrier( - vk::PipelineStageFlagBits::eTopOfPipe, - vk::PipelineStageFlagBits::eColorAttachmentOutput, - vk::DependencyFlags{}, - {}, - {}, - renderBarrier - ); - - // Begin dynamic rendering with vk::raii - commandBuffers[currentFrame].beginRendering(renderingInfo); - - // Set the viewport - vk::Viewport viewport(0.0f, 0.0f, - static_cast(swapChainExtent.width), - static_cast(swapChainExtent.height), - 0.0f, 1.0f); - commandBuffers[currentFrame].setViewport(0, viewport); - - // Set the scissor - vk::Rect2D scissor(vk::Offset2D(0, 0), swapChainExtent); - commandBuffers[currentFrame].setScissor(0, scissor); - - // Track current pipeline to avoid unnecessary bindings vk::raii::Pipeline* currentPipeline = nullptr; vk::raii::PipelineLayout* currentLayout = nullptr; std::vector blendedQueue; + std::unordered_set blendedSet; - // If loading, skip drawing scene entities (only clear and allow overlay) bool blockScene = false; if (imguiSystem) { - // Prefer renderer flag when available blockScene = IsLoading() || (GetTextureTasksScheduled() > 0 && GetTextureTasksCompleted() < GetTextureTasksScheduled()); } - // Render each entity (skip while loading) - if (!blockScene) - for (auto const& uptr : entities) { - Entity* entity = uptr.get(); - if (!entity || !entity->IsActive()) { - continue; - } - // Check if ball-only rendering is enabled and filter entities accordingly - if (imguiSystem && imguiSystem->IsBallOnlyRenderingEnabled()) { - // Only render entities whose names contain "Ball_" - if (entity->GetName().find("Ball_") == std::string::npos) { - continue; // Skip non-ball entities - } - } - - // Skip camera entities - they should not be rendered - if (entity->GetName() == "Camera") { - continue; - } - - // Get the mesh component - auto meshComponent = entity->GetComponent(); - if (!meshComponent) { - continue; - } - - // Get the transform component - auto transformComponent = entity->GetComponent(); - if (!transformComponent) { - continue; - } - - // Determine which pipeline to use - now defaults to BRDF/PBR instead of Phong - // Use basic pipeline only when PBR is explicitly disabled via ImGui - bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); - bool usePBR = !useBasic; // BRDF/PBR is now the default lighting model - - // Choose PBR pipeline variant per material (BLEND -> blended pipeline) - vk::raii::Pipeline* selectedPipeline = nullptr; - vk::raii::PipelineLayout* selectedLayout = nullptr; - if (usePBR) { + if (!blockScene) { + for (const auto& uptr : entities) { + Entity* entity = uptr.get(); + if (!entity || !entity->IsActive()) continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) continue; bool useBlended = false; if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { std::string entityName = entity->GetName(); size_t tagPos = entityName.find("_Material_"); if (tagPos != std::string::npos) { size_t afterTag = tagPos + std::string("_Material_").size(); - size_t sep = entityName.find('_', afterTag); - if (sep != std::string::npos && sep + 1 < entityName.length()) { - std::string materialName = entityName.substr(sep + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material) { - if (material->alphaMode == "BLEND") { - useBlended = true; - } else if (material->alphaMode != "MASK" && material->transmissionFactor > 0.001f) { - // Use blended pipeline for transmissive materials - useBlended = true; - } else if (material->useSpecularGlossiness && material->alpha < 0.999f) { - // SpecGloss glass with alpha < 1 should blend + if (afterTag < entityName.length()) { + // Entity name format: "modelName_Material__" + // Find the next underscore after the material index to get the actual material name + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { + std::string materialName = remainder.substr(nextUnderscore + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material && (material->alphaMode == "BLEND" || material->transmissionFactor > 0.001f)) { useBlended = true; } } } } } - // Defer blended/transmissive materials to a second pass if (useBlended) { blendedQueue.push_back(entity); - continue; + blendedSet.insert(entity); } - // Opaques use the non-blended PBR pipeline in this first pass - selectedPipeline = &pbrGraphicsPipeline; - selectedLayout = &pbrPipelineLayout; - } else { - selectedPipeline = &graphicsPipeline; - selectedLayout = &pipelineLayout; - } - - // Get the mesh resources - they should already exist from pre-allocation - auto meshIt = meshResources.find(meshComponent); - if (meshIt == meshResources.end()) { - std::cerr << "ERROR: Mesh resources not found for entity " << entity->GetName() - << " - resources should have been pre-allocated during scene loading!" << std::endl; - continue; } + } - // Get the entity resources - they should already exist from pre-allocation - auto entityIt = entityResources.find(entity); - if (entityIt == entityResources.end()) { - std::cerr << "ERROR: Entity resources not found for entity " << entity->GetName() - << " - resources should have been pre-allocated during scene loading!" << std::endl; - continue; - } - - // Bind pipeline if it changed - if (currentPipeline != selectedPipeline) { - commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); - currentPipeline = selectedPipeline; - currentLayout = selectedLayout; - } - - // Always bind both vertex and instance buffers since shaders expect instance data - // The instancing toggle controls the rendering behavior, not the buffer binding - std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; - std::array offsets = {0, 0}; - commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); - - // Always set UBO.model from the entity's transform; shaders combine it with instance matrices - updateUniformBuffer(currentFrame, entity, camera); - - // Bind the index buffer - commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); - - // Bind the descriptor set using the appropriate pipeline layout - auto& selectedDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; - - // Check if descriptor sets exist for the current pipeline type - if (selectedDescriptorSets.empty()) { - std::cerr << "Error: No descriptor sets available for entity " << entity->GetName() - << " (pipeline: " << (usePBR ? "PBR" : "basic") << ")" << std::endl; - continue; // Skip this entity - } - - if (currentFrame >= selectedDescriptorSets.size()) { - std::cerr << "Error: Invalid frame index " << currentFrame - << " for entity " << entity->GetName() - << " (descriptor sets size: " << selectedDescriptorSets.size() << ")" << std::endl; - continue; // Skip this entity - } - - commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {}); - - - // Set PBR material properties using push constants - if (usePBR) { - MaterialProperties pushConstants{}; + // Sort transparent entities back-to-front for correct blending of nested glass/liquids + if (!blendedQueue.empty()) { + // Sort by view-space depth using the camera's view matrix for robust back-to-front order + glm::mat4 V = camera ? camera->GetViewMatrix() : glm::mat4(1.0f); + std::sort(blendedQueue.begin(), blendedQueue.end(), [V](Entity* a, Entity* b) { + auto* ta = (a ? a->GetComponent() : nullptr); + auto* tb = (b ? b->GetComponent() : nullptr); + glm::vec3 pa = ta ? ta->GetPosition() : glm::vec3(0.0f); + glm::vec3 pb = tb ? tb->GetPosition() : glm::vec3(0.0f); + float za = (V * glm::vec4(pa, 1.0f)).z; + float zb = (V * glm::vec4(pb, 1.0f)).z; + if (za != zb) { + // In view space (looking down -Z), farther objects have more negative z. Sort ascending: farther first. + return za < zb; + } + // Fallback to stable ordering + return a < b; + }); + } - // Try to get material properties for this specific entity - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - // Extract material name from entity name for any GLTF model entities - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - // After the tag, there should be a numeric material index, then an underscore, then the material name - size_t sep = entityName.find('_', afterTag); - if (sep != std::string::npos && sep + 1 < entityName.length()) { - std::string materialName = entityName.substr(sep + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material) { - // Use actual PBR properties from the GLTF material - pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); - pushConstants.metallicFactor = material->metallic; - pushConstants.roughnessFactor = material->roughness; - pushConstants.emissiveFactor = material->emissive; // Set emissive factor for HDR emissive sources - pushConstants.emissiveStrength = material->emissiveStrength; // Set emissive strength from KHR_materials_emissive_strength extension - pushConstants.transmissionFactor = material->transmissionFactor; // KHR_materials_transmission - // SpecGloss workflow push constants - if (material->useSpecularGlossiness) { - pushConstants.useSpecGlossWorkflow = 1; - pushConstants.specularFactor = material->specularFactor; - pushConstants.glossinessFactor = material->glossinessFactor; - // If no SpecGloss texture, signal shader to use factors-only path - pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + // PASS 1: RENDER OPAQUE OBJECTS TO OFF-SCREEN TEXTURE + { + vk::ImageMemoryBarrier barrier{ .srcAccessMask = vk::AccessFlagBits::eNone, .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; + commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, barrier); + vk::RenderingAttachmentInfo colorAttachment{ .imageView = *opaqueSceneColorImageView, .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f}) }; + depthAttachment.imageView = *depthImageView; + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; + vk::RenderingInfo passInfo{ .renderArea = vk::Rect2D({0, 0}, swapChainExtent), .layerCount = 1, .colorAttachmentCount = 1, .pColorAttachments = &colorAttachment, .pDepthAttachment = &depthAttachment }; + commandBuffers[currentFrame].beginRendering(passInfo); + vk::Viewport viewport(0.0f, 0.0f, (float)swapChainExtent.width, (float)swapChainExtent.height, 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport); + vk::Rect2D scissor({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor); + if (!blockScene) { + for (const auto& uptr : entities) { + Entity* entity = uptr.get(); + if (!entity || !entity->IsActive() || blendedSet.count(entity)) continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) continue; + bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); + vk::raii::Pipeline* selectedPipeline = useBasic ? &graphicsPipeline : &pbrGraphicsPipeline; + vk::raii::PipelineLayout* selectedLayout = useBasic ? &pipelineLayout : &pbrPipelineLayout; + if (currentPipeline != selectedPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + } + auto meshIt = meshResources.find(meshComponent); + auto entityIt = entityResources.find(entity); + if (meshIt == meshResources.end() || entityIt == entityResources.end()) continue; + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + updateUniformBuffer(currentFrame, entity, camera); + auto& descSets = useBasic ? entityIt->second.basicDescriptorSets : entityIt->second.pbrDescriptorSets; + if (descSets.empty() || currentFrame >= descSets.size()) continue; + if (useBasic) { + // Basic pipeline expects only set 0 + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **currentLayout, + 0, + { *descSets[currentFrame] }, + {} + ); + } else { + // Opaque PBR pipeline: bind set 0 (PBR) and a valid set 1 (fallback scene color) + vk::DescriptorSet set1Opaque = *transparentFallbackDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **currentLayout, + 0, + { *descSets[currentFrame], set1Opaque }, + {} + ); + } + if (!useBasic) { + MaterialProperties pushConstants{}; + // Sensible defaults for entities without explicit material + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 0.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 0; // sample bound baseColor (falls back to shared default if none) + pushConstants.physicalDescriptorTextureSet = 0; // default to sampling metallic-roughness on binding 2 + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; + pushConstants.hasEmissiveStrengthExtension = false; // Default entities don't have emissive strength extension + pushConstants.transmissionFactor = 0.0f; + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.glossinessFactor = 0.0f; + pushConstants.specularFactor = glm::vec3(1.0f); + // pushConstants.ior already 1.5f default + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) { + // Entity name format: "modelName_Material__" + // Find the next underscore after the material index to get the actual material name + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { + std::string materialName = remainder.substr(nextUnderscore + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + // Base factors + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + + // Texture set flags (-1 = no texture) + pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; + // physical descriptor: MR or SpecGloss + if (material->useSpecularGlossiness) { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + pushConstants.glossinessFactor = material->glossinessFactor; + pushConstants.specularFactor = material->specularFactor; + } else { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; + } + pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; + pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; + pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; + + // Emissive and transmission/IOR + pushConstants.emissiveFactor = material->emissive; + pushConstants.emissiveStrength = material->emissiveStrength; + pushConstants.hasEmissiveStrengthExtension = false; // Material has emissive strength data + pushConstants.transmissionFactor = material->transmissionFactor; + pushConstants.ior = material->ior; + + // Alpha mask handling + pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } + } + } + } + } + // If no explicit MASK from a material, infer it from the baseColor texture's alpha usage + if (pushConstants.alphaMask < 0.5f) { + std::string baseColorPath; + if (meshComponent) { + if (!meshComponent->GetBaseColorTexturePath().empty()) { + baseColorPath = meshComponent->GetBaseColorTexturePath(); + } else if (!meshComponent->GetTexturePath().empty()) { + baseColorPath = meshComponent->GetTexturePath(); } else { - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.specularFactor = glm::vec3(0.04f); - pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; - pushConstants.physicalDescriptorTextureSet = 0; + baseColorPath = SHARED_DEFAULT_ALBEDO_ID; } } else { - // Default PBR material properties - pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); - pushConstants.metallicFactor = 0.1f; - pushConstants.roughnessFactor = 0.7f; - pushConstants.emissiveFactor = glm::vec3(0.0f); - pushConstants.emissiveStrength = 1.0f; + baseColorPath = SHARED_DEFAULT_ALBEDO_ID; } - } - } - } else { - // Default PBR material properties for non-GLTF entities - pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); - pushConstants.metallicFactor = 0.1f; - pushConstants.roughnessFactor = 0.7f; - pushConstants.emissiveFactor = glm::vec3(0.0f); - pushConstants.emissiveStrength = 0.0f; - pushConstants.transmissionFactor = 0.0f; - // Default to MR workflow - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.specularFactor = glm::vec3(0.04f); - pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; - } - - // Set texture binding indices - pushConstants.baseColorTextureSet = 0; - pushConstants.physicalDescriptorTextureSet = 0; - pushConstants.normalTextureSet = 0; - pushConstants.occlusionTextureSet = 0; - // For emissive: indicate absence with -1 so shader uses factor-only emissive - int emissiveSet = -1; - if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) { - emissiveSet = 0; - } - pushConstants.emissiveTextureSet = emissiveSet; - // Alpha mask from glTF material - pushConstants.alphaMask = 0.0f; - pushConstants.alphaMaskCutoff = 0.5f; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - size_t sep = entityName.find('_', afterTag); - if (sep != std::string::npos && sep + 1 < entityName.length()) { - std::string materialName = entityName.substr(sep + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material) { - if (material->alphaMode == "MASK") { + // Avoid inferring MASK from the shared default albedo (semi-transparent placeholder) + if (baseColorPath != SHARED_DEFAULT_ALBEDO_ID) { + const std::string resolvedBase = ResolveTextureId(baseColorPath); + std::shared_lock texLock(textureResourcesMutex); + auto itTex = textureResources.find(resolvedBase); + if (itTex != textureResources.end() && itTex->second.alphaMaskedHint) { pushConstants.alphaMask = 1.0f; - pushConstants.alphaMaskCutoff = material->alphaCutoff; - } else { - pushConstants.alphaMask = 0.0f; // OPAQUE or BLEND not handled here + pushConstants.alphaMaskCutoff = 0.5f; } } } + commandBuffers[currentFrame].pushConstants(**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, { pushConstants }); } + uint32_t instanceCount = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); } - - // Push constants to the shader - commandBuffers[currentFrame].pushConstants( - **currentLayout, - vk::ShaderStageFlagBits::eFragment, - 0, - vk::ArrayProxy(sizeof(MaterialProperties), reinterpret_cast(&pushConstants)) - ); } - - uint32_t instanceCount = static_cast(std::max(1u, static_cast(meshComponent->GetInstanceCount()))); - commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); + commandBuffers[currentFrame].endRendering(); } - - // Second pass: render blended/transmissive materials after opaque - if (!blendedQueue.empty()) { - for (Entity* entity : blendedQueue) { - if (!entity || !entity->IsActive()) { continue; } - - auto meshComponent = entity->GetComponent(); - if (!meshComponent) { continue; } - auto transformComponent = entity->GetComponent(); - if (!transformComponent) { continue; } - - // Mesh & entity resources - auto meshIt = meshResources.find(meshComponent); - if (meshIt == meshResources.end()) { - std::cerr << "ERROR: Mesh resources not found for blended entity " << entity->GetName() << std::endl; - continue; - } - auto entityIt = entityResources.find(entity); - if (entityIt == entityResources.end()) { - std::cerr << "ERROR: Entity resources not found for blended entity " << entity->GetName() << std::endl; - continue; - } - - // Use blended PBR pipeline - vk::raii::Pipeline* selectedPipeline = &pbrBlendGraphicsPipeline; - vk::raii::PipelineLayout* selectedLayout = &pbrPipelineLayout; - if (currentPipeline != selectedPipeline) { - commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); - currentPipeline = selectedPipeline; - currentLayout = selectedLayout; - } - - // Bind vertex + instance buffers - std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; - std::array offsets = {0, 0}; - commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); - - // Update UBO for this entity - updateUniformBuffer(currentFrame, entity, camera); - - // Bind index buffer - commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); - - // Bind descriptor set (PBR) - auto& selectedDescriptorSets = entityIt->second.pbrDescriptorSets; - if (selectedDescriptorSets.empty() || currentFrame >= selectedDescriptorSets.size()) { - std::cerr << "Error: No valid PBR descriptor set for blended entity " << entity->GetName() << std::endl; - continue; - } - commandBuffers[currentFrame].bindDescriptorSets( - vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {} - ); - - // Push PBR material properties (same as first pass) - MaterialProperties pushConstants{}; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - size_t sep = entityName.find('_', afterTag); - if (sep != std::string::npos && sep + 1 < entityName.length()) { - std::string materialName = entityName.substr(sep + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material) { - pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); - pushConstants.metallicFactor = material->metallic; - pushConstants.roughnessFactor = material->roughness; - pushConstants.emissiveFactor = material->emissive; - pushConstants.emissiveStrength = material->emissiveStrength; - pushConstants.transmissionFactor = material->transmissionFactor; - if (material->useSpecularGlossiness) { - pushConstants.useSpecGlossWorkflow = 1; - pushConstants.specularFactor = material->specularFactor; - pushConstants.glossinessFactor = material->glossinessFactor; - pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; - } else { - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.specularFactor = glm::vec3(0.04f); - pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; - pushConstants.physicalDescriptorTextureSet = 0; - } - } else { - pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); - pushConstants.metallicFactor = 0.1f; - pushConstants.roughnessFactor = 0.7f; - pushConstants.emissiveFactor = glm::vec3(0.0f); - pushConstants.emissiveStrength = 1.0f; - } - } - } - } else { - pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); - pushConstants.metallicFactor = 0.1f; - pushConstants.roughnessFactor = 0.7f; + // BARRIER AND COPY + { + vk::ImageMemoryBarrier opaqueSrcBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .dstAccessMask = vk::AccessFlagBits::eTransferRead, .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, .newLayout = vk::ImageLayout::eTransferSrcOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; + commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, opaqueSrcBarrier); + vk::ImageMemoryBarrier swapchainDstBarrier{ .srcAccessMask = vk::AccessFlagBits::eNone, .dstAccessMask = vk::AccessFlagBits::eTransferWrite, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eTransferDstOptimal, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; + commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, swapchainDstBarrier); + vk::ImageCopy copyRegion{ .srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, .dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, .extent = {swapChainExtent.width, swapChainExtent.height, 1} }; + commandBuffers[currentFrame].copyImage(*opaqueSceneColorImage, vk::ImageLayout::eTransferSrcOptimal, swapChainImages[imageIndex], vk::ImageLayout::eTransferDstOptimal, copyRegion); + vk::ImageMemoryBarrier opaqueShaderBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferRead, .dstAccessMask = vk::AccessFlagBits::eShaderRead, .oldLayout = vk::ImageLayout::eTransferSrcOptimal, .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; + commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, opaqueShaderBarrier); + vk::ImageMemoryBarrier swapchainTargetBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferWrite, .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .oldLayout = vk::ImageLayout::eTransferDstOptimal, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; + commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, swapchainTargetBarrier); + } + // PASS 2: RENDER TRANSPARENT OBJECTS TO THE SWAPCHAIN + { + colorAttachments[0].imageView = *swapChainImageViews[imageIndex]; + colorAttachments[0].loadOp = vk::AttachmentLoadOp::eLoad; + depthAttachment.loadOp = vk::AttachmentLoadOp::eLoad; + renderingInfo.renderArea = vk::Rect2D({0, 0}, swapChainExtent); + commandBuffers[currentFrame].beginRendering(renderingInfo); + vk::Viewport viewport(0.0f, 0.0f, (float)swapChainExtent.width, (float)swapChainExtent.height, 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport); + vk::Rect2D scissor({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor); + + if (!blendedQueue.empty()) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrBlendGraphicsPipeline); + currentLayout = &pbrTransparentPipelineLayout; + + for (Entity* entity : blendedQueue) { + auto meshComponent = entity->GetComponent(); + auto entityIt = entityResources.find(entity); + auto meshIt = meshResources.find(meshComponent); + if (!meshComponent || entityIt == entityResources.end() || meshIt == meshResources.end()) continue; + + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + updateUniformBuffer(currentFrame, entity, camera); + + auto& pbrDescSets = entityIt->second.pbrDescriptorSets; + if (pbrDescSets.empty() || currentFrame >= pbrDescSets.size()) continue; + + // Bind PBR (set 0) and scene color (set 1). If primary set 1 is unavailable, use fallback. + vk::DescriptorSet set1 = transparentDescriptorSets.empty() + ? *transparentFallbackDescriptorSets[currentFrame] + : *transparentDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **currentLayout, + 0, + { *pbrDescSets[currentFrame], set1 }, + {} + ); + + MaterialProperties pushConstants{}; + // Sensible defaults for entities without explicit material + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 0.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 0; // sample bound baseColor (falls back to shared default if none) + pushConstants.physicalDescriptorTextureSet = 0; // default to sampling metallic-roughness on binding 2 + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; pushConstants.emissiveFactor = glm::vec3(0.0f); - pushConstants.emissiveStrength = 0.0f; + pushConstants.emissiveStrength = 1.0f; + pushConstants.hasEmissiveStrengthExtension = false; pushConstants.transmissionFactor = 0.0f; pushConstants.useSpecGlossWorkflow = 0; - pushConstants.specularFactor = glm::vec3(0.04f); - pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; - } - pushConstants.baseColorTextureSet = 0; - pushConstants.physicalDescriptorTextureSet = 0; - pushConstants.normalTextureSet = 0; - pushConstants.occlusionTextureSet = 0; - int emissiveSet = -1; - if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) { emissiveSet = 0; } - pushConstants.emissiveTextureSet = emissiveSet; - pushConstants.alphaMask = 0.0f; - pushConstants.alphaMaskCutoff = 0.5f; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - size_t sep = entityName.find('_', afterTag); - if (sep != std::string::npos && sep + 1 < entityName.length()) { - std::string materialName = entityName.substr(sep + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material && material->alphaMode == "MASK") { - pushConstants.alphaMask = 1.0f; - pushConstants.alphaMaskCutoff = material->alphaCutoff; + pushConstants.glossinessFactor = 0.0f; + pushConstants.specularFactor = glm::vec3(1.0f); + // pushConstants.ior already 1.5f default + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) { + // Entity name format: "modelName_Material__" + // Find the next underscore after the material index to get the actual material name + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { + std::string materialName = remainder.substr(nextUnderscore + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + // Base factors + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + + // Texture set flags (-1 = no texture) + pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; + if (material->useSpecularGlossiness) { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + pushConstants.glossinessFactor = material->glossinessFactor; + pushConstants.specularFactor = material->specularFactor; + } else { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; + } + pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; + pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; + pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; + + // Emissive and transmission/IOR + pushConstants.emissiveFactor = material->emissive; + pushConstants.emissiveStrength = material->emissiveStrength; + pushConstants.hasEmissiveStrengthExtension = false; // Material has emissive strength data + pushConstants.transmissionFactor = material->transmissionFactor; + pushConstants.ior = material->ior; + + // Alpha mask handling + pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } + } } } } + commandBuffers[currentFrame].pushConstants(**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, { pushConstants }); + uint32_t instanceCountT = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCountT, 0, 0, 0); } - - commandBuffers[currentFrame].pushConstants( - **currentLayout, - vk::ShaderStageFlagBits::eFragment, - 0, - vk::ArrayProxy(sizeof(MaterialProperties), reinterpret_cast(&pushConstants)) - ); - - uint32_t instanceCount = static_cast(std::max(1u, static_cast(meshComponent->GetInstanceCount()))); - commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); } - } - // Render ImGui if provided - if (imguiSystem) { - imguiSystem->Render(commandBuffers[currentFrame], currentFrame); + if (imguiSystem) { + imguiSystem->Render(commandBuffers[currentFrame], currentFrame); + } + commandBuffers[currentFrame].endRendering(); } - // End dynamic rendering - commandBuffers[currentFrame].endRendering(); - - // Transition swapchain image layout for presentation - vk::ImageMemoryBarrier imageBarrier{ - .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, - .dstAccessMask = vk::AccessFlagBits::eNone, - .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, - .newLayout = vk::ImageLayout::ePresentSrcKHR, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = swapChainImages[imageIndex], - .subresourceRange = { - .aspectMask = vk::ImageAspectFlagBits::eColor, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 - } - }; - - commandBuffers[currentFrame].pipelineBarrier( - vk::PipelineStageFlagBits::eColorAttachmentOutput, - vk::PipelineStageFlagBits::eBottomOfPipe, - vk::DependencyFlags{}, - {}, - {}, - imageBarrier - ); - - // End command buffer + // ... (final barrier, submit, and present logic is the same) ... + vk::ImageMemoryBarrier presentBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .dstAccessMask = vk::AccessFlagBits::eNone, .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, .newLayout = vk::ImageLayout::ePresentSrcKHR, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; + commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eBottomOfPipe, {}, {}, {}, presentBarrier); commandBuffers[currentFrame].end(); - - // Submit command buffer - // Wait for both image availability (binary) and all completed texture uploads (timeline) std::array waitSems = { *imageAvailableSemaphores[currentFrame], *uploadsTimeline }; std::array waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader }; - uint64_t uploadsValueToWait = uploadTimelineValue.load(std::memory_order_relaxed); + uint64_t uploadsValueToWait = uploadTimelineLastSubmitted.load(std::memory_order_relaxed); std::array waitValues = { 0ull, uploadsValueToWait }; - vk::TimelineSemaphoreSubmitInfo timelineWaitInfo{ - .waitSemaphoreValueCount = static_cast(waitValues.size()), - .pWaitSemaphoreValues = waitValues.data() - }; - vk::SubmitInfo submitInfo{ - .pNext = &timelineWaitInfo, - .waitSemaphoreCount = static_cast(waitSems.size()), - .pWaitSemaphores = waitSems.data(), - .pWaitDstStageMask = waitStages.data(), - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffers[currentFrame], - .signalSemaphoreCount = 1, - .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex] - }; - - // Use mutex to ensure thread-safe access to graphics queue - { - std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + vk::TimelineSemaphoreSubmitInfo timelineWaitInfo{ .waitSemaphoreValueCount = (uint32_t)waitValues.size(), .pWaitSemaphoreValues = waitValues.data() }; + vk::SubmitInfo submitInfo{ .pNext = &timelineWaitInfo, .waitSemaphoreCount = (uint32_t)waitSems.size(), .pWaitSemaphores = waitSems.data(), .pWaitDstStageMask = waitStages.data(), .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex] }; + if (framebufferResized.load(std::memory_order_relaxed)) { + vk::SubmitInfo emptySubmit{}; + { std::lock_guard lock(queueMutex); graphicsQueue.submit(emptySubmit, *inFlightFences[currentFrame]); } + recreateSwapChain(); + return; } - - // Present the image - vk::PresentInfoKHR presentInfo{ - .waitSemaphoreCount = 1, - .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], - .swapchainCount = 1, - .pSwapchains = &*swapChain, - .pImageIndices = &imageIndex - }; - - // Use mutex to ensure thread-safe access to present queue + { std::lock_guard lock(queueMutex); graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); } + vk::PresentInfoKHR presentInfo{ .waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex }; try { std::lock_guard lock(queueMutex); result.first = presentQueue.presentKHR(presentInfo); } catch (const vk::OutOfDateKHRError&) { - framebufferResized = true; + framebufferResized.store(true, std::memory_order_relaxed); } - - if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { - framebufferResized = false; + if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { + framebufferResized.store(false, std::memory_order_relaxed); recreateSwapChain(); } else if (result.first != vk::Result::eSuccess) { throw std::runtime_error("Failed to present swap chain image"); } - - // Advance to the next frame currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; } diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index caba414b..32201bd7 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -62,6 +62,30 @@ bool Renderer::createDepthResources() { } } +// Helper: coerce an sRGB/UNORM variant of a given VkFormat while preserving block type where possible +static vk::Format CoerceFormatSRGB(vk::Format fmt, bool wantSRGB) { + switch (fmt) { + case vk::Format::eR8G8B8A8Unorm: return wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + case vk::Format::eR8G8B8A8Srgb: return wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + + case vk::Format::eBc1RgbUnormBlock: return wantSRGB ? vk::Format::eBc1RgbSrgbBlock : vk::Format::eBc1RgbUnormBlock; + case vk::Format::eBc1RgbSrgbBlock: return wantSRGB ? vk::Format::eBc1RgbSrgbBlock : vk::Format::eBc1RgbUnormBlock; + case vk::Format::eBc1RgbaUnormBlock: return wantSRGB ? vk::Format::eBc1RgbaSrgbBlock : vk::Format::eBc1RgbaUnormBlock; + case vk::Format::eBc1RgbaSrgbBlock: return wantSRGB ? vk::Format::eBc1RgbaSrgbBlock : vk::Format::eBc1RgbaUnormBlock; + + case vk::Format::eBc2UnormBlock: return wantSRGB ? vk::Format::eBc2SrgbBlock : vk::Format::eBc2UnormBlock; + case vk::Format::eBc2SrgbBlock: return wantSRGB ? vk::Format::eBc2SrgbBlock : vk::Format::eBc2UnormBlock; + + case vk::Format::eBc3UnormBlock: return wantSRGB ? vk::Format::eBc3SrgbBlock : vk::Format::eBc3UnormBlock; + case vk::Format::eBc3SrgbBlock: return wantSRGB ? vk::Format::eBc3SrgbBlock : vk::Format::eBc3UnormBlock; + + case vk::Format::eBc7UnormBlock: return wantSRGB ? vk::Format::eBc7SrgbBlock : vk::Format::eBc7UnormBlock; + case vk::Format::eBc7SrgbBlock: return wantSRGB ? vk::Format::eBc7SrgbBlock : vk::Format::eBc7UnormBlock; + + default: return fmt; + } +} + // Create texture image bool Renderer::createTextureImage(const std::string& texturePath_, TextureResources& resources) { try { @@ -276,32 +300,45 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour stagingBufferMemory.unmapMemory(); - // Free pixel data - if (isKtx2) { - ktxTexture_Destroy((ktxTexture*)ktxTex); - } else { - // no-op: non-KTX path disabled - } // Determine appropriate texture format vk::Format textureFormat; + const bool wantSRGB = (Renderer::determineTextureFormat(textureId) == vk::Format::eR8G8B8A8Srgb); + bool alphaMaskedHint = false; if (isKtx2) { - // If the KTX2 provided a valid VkFormat and we did NOT transcode, use it (may be a GPU compressed format) + // If the KTX2 provided a valid VkFormat and we did NOT transcode, respect its block type + // but coerce the sRGB/UNORM variant based on texture usage (baseColor vs data maps) if (!wasTranscoded) { VkFormat headerFmt = static_cast(headerVkFormatRaw); if (headerFmt != VK_FORMAT_UNDEFINED) { - textureFormat = static_cast(headerFmt); + textureFormat = CoerceFormatSRGB(static_cast(headerFmt), wantSRGB); } else { - textureFormat = Renderer::determineTextureFormat(textureId); + textureFormat = wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; } + // Can't easily scan alpha in compressed formats here; leave hint at default false } else { // Transcoded to RGBA32; choose SRGB/UNORM by heuristic - textureFormat = Renderer::determineTextureFormat(textureId); + textureFormat = wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + // We have CPU-visible RGBA data in 'levelData' above; scan alpha for masking hint + if (ktxTex) { + ktx_size_t offsetScan = 0; + ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offsetScan); + const uint8_t* rgba = ktxTexture_GetData((ktxTexture*)ktxTex) + offsetScan; + size_t pixelCount = static_cast(texWidth) * static_cast(texHeight); + for (size_t i = 0; i < pixelCount; ++i) { + if (rgba[i * 4 + 3] < 250) { alphaMaskedHint = true; break; } + } + } } } else { - textureFormat = Renderer::determineTextureFormat(textureId); + textureFormat = wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; } + // Now that we're done reading libktx data, destroy the KTX texture to avoid leaks + if (isKtx2 && ktxTex) { + ktxTexture_Destroy((ktxTexture*)ktxTex); + ktxTex = nullptr; + } // Create texture image using memory pool auto [textureImg, textureImgAllocation] = createImagePooled( @@ -316,12 +353,13 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour resources.textureImage = std::move(textureImg); resources.textureImageAllocation = std::move(textureImgAllocation); - // GPU upload for this texture (no global serialization) + // GPU upload for this texture uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, copyRegions, mipLevels); // Store the format and mipLevels for createTextureImageView resources.format = textureFormat; resources.mipLevels = mipLevels; + resources.alphaMaskedHint = alphaMaskedHint; // Create texture image view if (!createTextureImageView(resources)) { @@ -586,7 +624,18 @@ vk::Format Renderer::determineTextureFormat(const std::string& textureId) { return vk::Format::eR8G8B8A8Srgb; } - // All other PBR textures (normal, metallic-roughness, occlusion, emissive) should be linear + // Emissive is color data and should be sampled in sRGB + if (idLower.find("emissive") != std::string::npos || + textureId == Renderer::SHARED_DEFAULT_EMISSIVE_ID) { + return vk::Format::eR8G8B8A8Srgb; + } + + // Shared bright red (ball) is a color texture; ensure sRGB for vivid appearance + if (textureId == Renderer::SHARED_BRIGHT_RED_ID) { + return vk::Format::eR8G8B8A8Srgb; + } + + // All other PBR textures (normal, metallic-roughness, occlusion) should be linear // because they contain non-color data that shouldn't be gamma corrected return vk::Format::eR8G8B8A8Unorm; } @@ -689,6 +738,12 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne return false; } + // Analyze alpha to set alphaMaskedHint (treat as masked if any pixel alpha < ~1.0) + bool alphaMaskedHint = false; + for (int i = 0, n = width * height; i < n; ++i) { + if (stagingData[i * 4 + 3] < 250) { alphaMaskedHint = true; break; } + } + stagingBufferMemory.unmapMemory(); // Determine the appropriate texture format based on the texture type @@ -707,7 +762,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne resources.textureImage = std::move(textureImg); resources.textureImageAllocation = std::move(textureImgAllocation); - // GPU upload (no global serialization). Copy buffer to image in a single submit. + // GPU upload. Copy buffer to image in a single submit. std::vector regions = {{ .bufferOffset = 0, .bufferRowLength = 0, @@ -725,6 +780,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigne // Store the format for createTextureImageView resources.format = textureFormat; + resources.alphaMaskedHint = alphaMaskedHint; // Use resolvedId as the cache key to avoid duplicates const std::string& cacheId = resolvedId; @@ -976,398 +1032,82 @@ bool Renderer::createDescriptorPool() { bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePath, bool usePBR) { // Resolve alias before taking the shared lock to avoid nested shared_lock on the same mutex const std::string resolvedTexturePath = ResolveTextureId(texturePath); - // Guard textureResources access while resolving texture handles std::shared_lock texLock(textureResourcesMutex); try { - // Get entity resources auto entityIt = entityResources.find(entity); - if (entityIt == entityResources.end()) { - std::cerr << "Entity resources not found" << std::endl; - return false; - } + if (entityIt == entityResources.end()) return false; - // Get texture resources - use default texture as fallback if specific texture fails - TextureResources* textureRes = nullptr; - if (!texturePath.empty()) { - auto textureIt = textureResources.find(resolvedTexturePath); - if (textureIt == textureResources.end()) { - // If this is a GLTF embedded texture ID, don't try to load from disk - if (texturePath.rfind("gltf_", 0) == 0) { - // Handle both gltf_baseColor_{i} and gltf_basecolor_{i} - const std::string prefixUpper = "gltf_baseColor_"; - const std::string prefixLower = "gltf_basecolor_"; - if (texturePath.rfind(prefixUpper, 0) == 0 || texturePath.rfind(prefixLower, 0) == 0) { - const bool isUpper = texturePath.rfind(prefixUpper, 0) == 0; - std::string index = texturePath.substr((isUpper ? prefixUpper.size() : prefixLower.size())); - // Try direct baseColor id first - std::string baseColorId = "gltf_baseColor_" + index; - auto bcIt = textureResources.find(baseColorId); - if (bcIt != textureResources.end()) { - textureRes = &bcIt->second; - } else { - // Try alias to generic gltf_texture_{index} - std::string alias = "gltf_texture_" + index; - auto aliasIt = textureResources.find(alias); - if (aliasIt != textureResources.end()) { - textureRes = &aliasIt->second; - } else { - std::cerr << "Warning: Embedded texture not found: " << texturePath - << " (also missing alias: " << alias << ") using default." << std::endl; - textureRes = &defaultTextureResources; - } - } - } else { - std::cerr << "Warning: Embedded texture not found: " << texturePath << ", using default." << std::endl; - textureRes = &defaultTextureResources; - } - } else { - // Texture not yet available; bind default texture for now. - textureRes = &defaultTextureResources; - } - } else { - textureRes = &textureIt->second; - } - } else { - // No texture path specified, use default texture - textureRes = &defaultTextureResources; - } - - // Create descriptor sets using RAII - choose layout based on pipeline type vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; - std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); - vk::DescriptorSetAllocateInfo allocInfo{ - .descriptorPool = *descriptorPool, - .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), - .pSetLayouts = layouts.data() - }; + std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); + vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, .descriptorSetCount = MAX_FRAMES_IN_FLIGHT, .pSetLayouts = layouts.data() }; - // Choose the appropriate descriptor set vector based on pipeline type auto& targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; - - // Only create descriptor sets if they don't already exist for this pipeline type if (targetDescriptorSets.empty()) { - try { - // Allocate descriptor sets using RAII wrapper - vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); - - // Convert to vector of individual RAII descriptor sets - targetDescriptorSets.clear(); - targetDescriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - targetDescriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); - } - } catch (const std::exception& e) { - std::cerr << "Failed to allocate descriptor sets for entity " << entity->GetName() - << " (pipeline: " << (usePBR ? "PBR" : "basic") << "): " << e.what() << std::endl; - return false; - } - } - - // Validate descriptor sets before using them - if (targetDescriptorSets.size() != MAX_FRAMES_IN_FLIGHT) { - std::cerr << "Invalid descriptor set count for entity " << entity->GetName() - << " (expected: " << MAX_FRAMES_IN_FLIGHT << ", got: " << targetDescriptorSets.size() << ")" << std::endl; - return false; - } - - // Validate default texture resources before using them - if (!*defaultTextureResources.textureSampler || !*defaultTextureResources.textureImageView) { - std::cerr << "Invalid default texture resources for entity " << entity->GetName() << std::endl; - return false; + targetDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); } - // Update descriptor sets for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - // Validate descriptor set handle before using it - if (!*targetDescriptorSets[i]) { - std::cerr << "Invalid descriptor set handle for entity " << entity->GetName() - << " at frame " << i << " (pipeline: " << (usePBR ? "PBR" : "basic") << ")" << std::endl; - return false; - } - - // Validate uniform buffer before creating descriptor - if (i >= entityIt->second.uniformBuffers.size() || - !*entityIt->second.uniformBuffers[i]) { - std::cerr << "Invalid uniform buffer for entity " << entity->GetName() - << " at frame " << i << std::endl; - return false; - } - - // Uniform buffer descriptor - vk::DescriptorBufferInfo bufferInfo{ - .buffer = *entityIt->second.uniformBuffers[i], - .offset = 0, - .range = sizeof(UniformBufferObject) - }; + vk::DescriptorBufferInfo bufferInfo{ .buffer = *entityIt->second.uniformBuffers[i], .range = sizeof(UniformBufferObject) }; if (usePBR) { - // PBR pipeline: Create 7 descriptor writes (UBO + 5 textures + light storage buffer) + // PBR sets now only have 7 bindings (0-6) std::array descriptorWrites; std::array imageInfos; - // Uniform buffer descriptor writes (binding 0) - descriptorWrites[0] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pBufferInfo = &bufferInfo - }; + descriptorWrites[0] = { .dstSet = *targetDescriptorSets[i], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo }; - // Get all PBR texture paths from the entity's MeshComponent auto meshComponent = entity->GetComponent(); - // Resolve baseColor path with multiple fallbacks: GLTF baseColor -> legacy texturePath -> material DB -> shared default - std::string resolvedBaseColor; - if (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) { - resolvedBaseColor = meshComponent->GetBaseColorTexturePath(); - } else if (meshComponent && !meshComponent->GetTexturePath().empty()) { - resolvedBaseColor = meshComponent->GetTexturePath(); - } else { - // Try to use material name from entity name to query ModelLoader - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - // Expect format: _Material__ - size_t sep = entityName.find('_', afterTag); - if (sep != std::string::npos && sep + 1 < entityName.length()) { - std::string materialName = entityName.substr(sep + 1); - if (modelLoader) { - Material* mat = modelLoader->GetMaterial(materialName); - if (mat && !mat->albedoTexturePath.empty()) { - resolvedBaseColor = mat->albedoTexturePath; - } - } - } - } - if (resolvedBaseColor.empty()) { - resolvedBaseColor = SHARED_DEFAULT_ALBEDO_ID; - } + std::vector pbrTexturePaths = { /* ... same as before ... */ }; + // ... (logic to get texture paths is the same) + { + const std::string legacyPath = (meshComponent ? meshComponent->GetTexturePath() : std::string()); + const std::string baseColorPath = (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) + ? meshComponent->GetBaseColorTexturePath() + : (!legacyPath.empty() ? legacyPath : SHARED_DEFAULT_ALBEDO_ID); + const std::string mrPath = (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) + ? meshComponent->GetMetallicRoughnessTexturePath() + : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; + const std::string normalPath = (meshComponent && !meshComponent->GetNormalTexturePath().empty()) + ? meshComponent->GetNormalTexturePath() + : SHARED_DEFAULT_NORMAL_ID; + const std::string occlusionPath = (meshComponent && !meshComponent->GetOcclusionTexturePath().empty()) + ? meshComponent->GetOcclusionTexturePath() + : SHARED_DEFAULT_OCCLUSION_ID; + const std::string emissivePath = (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) + ? meshComponent->GetEmissiveTexturePath() + : SHARED_DEFAULT_EMISSIVE_ID; + + pbrTexturePaths = { baseColorPath, mrPath, normalPath, occlusionPath, emissivePath }; } - // Heuristic: if still default and we have an external normal map like *_ddna.ktx2, try to guess base color sibling - if (resolvedBaseColor == SHARED_DEFAULT_ALBEDO_ID && meshComponent) { - std::string normalPath = meshComponent->GetNormalTexturePath(); - if (!normalPath.empty() && normalPath.rfind("gltf_", 0) != 0) { - // Make a lowercase copy for pattern checks - std::string normalLower = normalPath; - std::transform(normalLower.begin(), normalLower.end(), normalLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - if (normalLower.find("_ddna") != std::string::npos) { - // Try replacing _ddna with common diffuse/basecolor suffixes - std::vector suffixes = {"_d", "_c", "_cm", "_diffuse", "_basecolor"}; - for (const auto& suf : suffixes) { - std::string candidate = normalPath; - // Replace only the first occurrence of _ddna - size_t pos = normalLower.find("_ddna"); - if (pos != std::string::npos) { - candidate.replace(pos, 5, suf); - // Attempt to load; if successful, use this as resolved base color - // On-demand loading disabled; skip attempting to load candidate - (void)candidate; // suppress unused - break; - } - } - } - } - } - - std::vector pbrTexturePaths = { - // Binding 1: baseColor - resolvedBaseColor, - // Binding 2: metallicRoughness - use GLTF texture or fallback to shared default - (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) ? - meshComponent->GetMetallicRoughnessTexturePath() : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID, - // Binding 3: normal - use GLTF texture or fallback to shared default - (meshComponent && !meshComponent->GetNormalTexturePath().empty()) ? - meshComponent->GetNormalTexturePath() : SHARED_DEFAULT_NORMAL_ID, - // Binding 4: occlusion - use GLTF texture or fallback to shared default - (meshComponent && !meshComponent->GetOcclusionTexturePath().empty()) ? - meshComponent->GetOcclusionTexturePath() : SHARED_DEFAULT_OCCLUSION_ID, - // Binding 5: emissive - use GLTF texture or fallback to shared default - (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) ? - meshComponent->GetEmissiveTexturePath() : SHARED_DEFAULT_EMISSIVE_ID - }; - // Create image infos for each PBR texture binding for (int j = 0; j < 5; j++) { - const std::string& currentTexturePath = pbrTexturePaths[j]; - const std::string resolvedBindingPath = ResolveTextureId(currentTexturePath); - - // Find the texture resources for this binding + const auto resolvedBindingPath = ResolveTextureId(pbrTexturePaths[j]); auto textureIt = textureResources.find(resolvedBindingPath); - if (textureIt != textureResources.end()) { - // Use the specific texture for this binding - const auto& texRes = textureIt->second; - - // Validate texture resources before using them (check if RAII objects are valid) - if (*texRes.textureSampler == VK_NULL_HANDLE || *texRes.textureImageView == VK_NULL_HANDLE) { - std::cerr << "Invalid texture resources for " << currentTexturePath - << " in entity " << entity->GetName() << ", using default texture" << std::endl; - // Fall back to default texture - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } else { - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *texRes.textureSampler, - .imageView = *texRes.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } - } else { - // On-demand texture loading disabled; use alias or defaults below - // Try alias for embedded baseColor textures: gltf_baseColor_{i} -> gltf_texture_{i} - if (currentTexturePath.rfind("gltf_baseColor_", 0) == 0 || - currentTexturePath.rfind("gltf_basecolor_", 0) == 0) { - std::string prefix = (currentTexturePath.rfind("gltf_baseColor_", 0) == 0) - ? std::string("gltf_baseColor_") - : std::string("gltf_basecolor_"); - std::string index = currentTexturePath.substr(prefix.size()); - std::string alias = "gltf_texture_" + index; - auto aliasIt = textureResources.find(alias); - if (aliasIt != textureResources.end()) { - const auto& texRes = aliasIt->second; - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *texRes.textureSampler, - .imageView = *texRes.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } else { - // Fall back to default white texture if the specific texture is not found - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } - } else { - // Fall back to default white texture if the specific texture is not found - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } - } - descriptor_path_resolved: ; - } - - // Create descriptor writes for all 5 texture bindings - for (int binding = 1; binding <= 5; binding++) { - descriptorWrites[binding] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = static_cast(binding), - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = &imageInfos[binding - 1] - }; + TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + imageInfos[j] = { .sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal }; + descriptorWrites[j + 1] = { .dstSet = *targetDescriptorSets[i], .dstBinding = static_cast(j + 1), .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfos[j] }; } - // No shadow maps: binding 6 is now the light storage buffer - - // Create descriptor write for light storage buffer (binding 6) - // Check if light storage buffers are initialized - if (i < lightStorageBuffers.size() && *lightStorageBuffers[i].buffer) { - vk::DescriptorBufferInfo lightBufferInfo{ - .buffer = *lightStorageBuffers[i].buffer, - .offset = 0, - .range = VK_WHOLE_SIZE - }; - - descriptorWrites[6] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = 6, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .pBufferInfo = &lightBufferInfo - }; - } else { - // Ensure light storage buffers are initialized before creating descriptor sets - // Initialize with at least 1 light to create the buffers - if (!createOrResizeLightStorageBuffers(1)) { - std::cerr << "Failed to initialize light storage buffers for descriptor set creation" << std::endl; - return false; - } - - // Now use the properly initialized light storage buffer - vk::DescriptorBufferInfo lightBufferInfo{ - .buffer = *lightStorageBuffers[i].buffer, - .offset = 0, - .range = VK_WHOLE_SIZE - }; - - descriptorWrites[6] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = 6, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .pBufferInfo = &lightBufferInfo - }; - } + vk::DescriptorBufferInfo lightBufferInfo{ .buffer = *lightStorageBuffers[i].buffer, .range = VK_WHOLE_SIZE }; + descriptorWrites[6] = { .dstSet = *targetDescriptorSets[i], .dstBinding = 6, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightBufferInfo }; - // Update descriptor sets with all 7 descriptors device.updateDescriptorSets(descriptorWrites, {}); - } else { - // Basic pipeline: Create 2 descriptor writes (UBO + 1 texture) - std::array descriptorWrites; - - // Uniform buffer descriptor write - descriptorWrites[0] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pBufferInfo = &bufferInfo + } else { // Basic Pipeline + // ... (this part remains the same) + auto textureIt = textureResources.find(resolvedTexturePath); + TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + vk::DescriptorImageInfo imageInfo{ .sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal }; + std::array descriptorWrites = { + vk::WriteDescriptorSet{ .dstSet = *targetDescriptorSets[i], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo }, + vk::WriteDescriptorSet{ .dstSet = *targetDescriptorSets[i], .dstBinding = 1, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo } }; - - // Check if texture resources are valid - bool hasValidTexture = !texturePath.empty() && textureRes && - *textureRes->textureSampler && - *textureRes->textureImageView; - - // Texture sampler descriptor - vk::DescriptorImageInfo imageInfo; - if (hasValidTexture) { - // Use provided texture resources - imageInfo = vk::DescriptorImageInfo{ - .sampler = *textureRes->textureSampler, - .imageView = *textureRes->textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } else { - // Use default texture resources - imageInfo = vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } - - // Texture sampler descriptor write - descriptorWrites[1] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = &imageInfo - }; - - // Update descriptor sets with both descriptors device.updateDescriptorSets(descriptorWrites, {}); } } - return true; } catch (const std::exception& e) { - std::cerr << "Failed to create descriptor sets: " << e.what() << std::endl; + std::cerr << "Failed to create descriptor sets for " << entity->GetName() << ": " << e.what() << std::endl; return false; } } @@ -1500,6 +1240,101 @@ std::pair Renderer::createBuffer( } } +void Renderer::createTransparentDescriptorSets() { + // We need one descriptor set per frame in flight for this resource + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *transparentDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; + + transparentDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + + // Update each descriptor set to point to our single off-screen opaque color image + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::DescriptorImageInfo imageInfo{ + .sampler = *opaqueSceneColorSampler, + .imageView = *opaqueSceneColorImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *transparentDescriptorSets[i], + .dstBinding = 0, // Binding 0 in Set 1 + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + device.updateDescriptorSets(descriptorWrite, nullptr); + } +} + +void Renderer::createTransparentFallbackDescriptorSets() { + // Allocate one descriptor set per frame in flight using the same layout (single combined image sampler at binding 0) + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *transparentDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; + + transparentFallbackDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + + // Point each set to the default texture, which is guaranteed to be in SHADER_READ_ONLY_OPTIMAL when used in the opaque pass + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::DescriptorImageInfo imageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *transparentFallbackDescriptorSets[i], + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + device.updateDescriptorSets(descriptorWrite, nullptr); + } +} + +bool Renderer::createOpaqueSceneColorResources() { + try { + // Create the image + auto [image, allocation] = createImagePooled( + swapChainExtent.width, + swapChainExtent.height, + swapChainImageFormat, // Use the same format as the swapchain + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, // <-- Note the new usage flags + vk::MemoryPropertyFlagBits::eDeviceLocal); + + opaqueSceneColorImage = std::move(image); + // We don't need a member for the allocation, it's managed by the unique_ptr + + // Create the image view + opaqueSceneColorImageView = createImageView(opaqueSceneColorImage, swapChainImageFormat, vk::ImageAspectFlagBits::eColor); + + // Create the sampler + vk::SamplerCreateInfo samplerInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge, + }; + opaqueSceneColorSampler = vk::raii::Sampler(device, samplerInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create opaque scene color resources: " << e.what() << std::endl; + return false; + } +} + // Copy buffer void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size) { ensureThreadLocalVulkanInit(); @@ -1764,10 +1599,10 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector& regions) const { ensureThreadLocalVulkanInit(); try { - // Create a temporary transient command pool for the transfer queue + // Create a temporary transient command pool for the GRAPHICS queue to avoid cross-queue races vk::CommandPoolCreateInfo poolInfo{ .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, - .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() }; vk::raii::CommandPool tempPool(device, poolInfo); vk::CommandBufferAllocateInfo allocInfo{ @@ -1808,7 +1643,7 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { std::lock_guard lock(queueMutex); - transferQueue.submit(submitInfo, *fence); + graphicsQueue.submit(submitInfo, *fence); } device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); } catch (const std::exception& e) { @@ -2014,6 +1849,9 @@ std::future Renderer::LoadTextureAsync(const std::string& texturePath) { textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); return ok; }; + + // Use shared_lock to prevent race condition with threadPool destruction + std::shared_lock lock(threadPoolMutex); if (!threadPool) { return std::async(std::launch::async, task); } @@ -2036,6 +1874,9 @@ std::future Renderer::LoadTextureFromMemoryAsync(const std::string& textur textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); return ok; }; + + // Use shared_lock to prevent race condition with threadPool destruction + std::shared_lock lock(threadPoolMutex); if (!threadPool) { return std::async(std::launch::async, std::move(task)); } @@ -2051,10 +1892,10 @@ void Renderer::uploadImageFromStaging(vk::Buffer staging, uint32_t mipLevels) { ensureThreadLocalVulkanInit(); try { - // Use a temporary transient command pool for the transfer queue family + // Use a temporary transient command pool for the GRAPHICS queue family to avoid cross-queue races vk::CommandPoolCreateInfo poolInfo{ .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, - .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() }; vk::raii::CommandPool tempPool(device, poolInfo); vk::CommandBufferAllocateInfo allocInfo{ @@ -2113,7 +1954,7 @@ void Renderer::uploadImageFromStaging(vk::Buffer staging, } }; toShader.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - toShader.dstAccessMask = vk::AccessFlagBits::eNone; // cannot use ShaderRead on transfer queue; visibility handled via timeline wait on graphics + // Keep dstAccessMask empty; visibility is ensured via submission ordering and timeline wait cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlagBits::eByRegion, @@ -2121,24 +1962,27 @@ void Renderer::uploadImageFromStaging(vk::Buffer staging, cb.end(); - // Submit once on the transfer queue, signal timeline semaphore, and wait fence (for safety) - uint64_t signalValue = uploadTimelineValue.fetch_add(1, std::memory_order_relaxed) + 1; - vk::TimelineSemaphoreSubmitInfo timelineInfo{ - .signalSemaphoreValueCount = 1, - .pSignalSemaphoreValues = &signalValue - }; - vk::SubmitInfo submit{ - .pNext = &timelineInfo, - .commandBufferCount = 1, - .pCommandBuffers = &*cb, - .signalSemaphoreCount = 1, - .pSignalSemaphores = &*uploadsTimeline - }; + // Submit once on the GRAPHICS queue; signal uploads timeline if available vk::raii::Fence fence(device, vk::FenceCreateInfo{}); - // Prefer dedicated transfer queue + bool canSignalTimeline = uploadsTimeline != nullptr; + uint64_t signalValue = 0; { std::lock_guard lock(queueMutex); - transferQueue.submit(submit, *fence); + vk::SubmitInfo submit{}; + if (canSignalTimeline) { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue + }; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + + graphicsQueue.submit(submit, *fence); } device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); } catch (const std::exception& e) { diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 79774080..bd7aeda0 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -4,10 +4,7 @@ #include "mesh_component.h" #include "camera_component.h" #include -#include -#include #include -#include #include /** @@ -33,13 +30,14 @@ glm::vec3 CalculateBoundingBoxSize(const MaterialMesh& materialMesh) { /** * @brief Load a GLTF model synchronously on the main thread. + * @return success or fail on loading the GLTF model. * @param engine The engine to create entities in. * @param modelPath The path to the GLTF model file. * @param position The position to place the model (default: origin with slight Y offset). * @param rotation The rotation to apply to the model (default: no rotation). * @param scale The scale to apply to the model (default: unit scale). */ -void LoadGLTFModel(Engine* engine, const std::string& modelPath, +bool LoadGLTFModel(Engine* engine, const std::string& modelPath, const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale) { // Get the model loader and renderer ModelLoader* modelLoader = engine->GetModelLoader(); @@ -48,10 +46,10 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, if (!modelLoader || !renderer) { std::cerr << "Error: ModelLoader or Renderer is null" << std::endl; if (renderer) { renderer->SetLoading(false); } - return; + return false; } // Ensure loading flag is cleared on any exit from this function - struct LoadingGuard { Renderer* r; ~LoadingGuard(){ if (r) r->SetLoading(false); } } loadingGuard{renderer}; + struct LoadingGuard { Renderer* r; ~LoadingGuard(){ r->SetLoading(false); } } loadingGuard{renderer}; // Extract model name from file path for entity naming std::filesystem::path modelFilePath(modelPath); @@ -62,7 +60,7 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, Model* loadedModel = modelLoader->LoadGLTF(modelPath); if (!loadedModel) { std::cerr << "Failed to load GLTF model: " << modelPath << std::endl; - return; + return false; } std::cout << "Successfully loaded GLTF model with all textures and lighting: " << modelPath << std::endl; @@ -79,12 +77,11 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, transformMatrix = glm::scale(transformMatrix, scale); // Transform all light positions from local model space to world space + // Also transform the light direction (for directional lights) + glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(transformMatrix))); for (auto& light : extractedLights) { glm::vec4 worldPos = transformMatrix * glm::vec4(light.position, 1.0f); light.position = glm::vec3(worldPos); - - // Also transform the light direction (for directional lights) - glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(transformMatrix))); light.direction = glm::normalize(normalMatrix * light.direction); } @@ -146,10 +143,9 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, const std::vector& materialMeshes = modelLoader->GetMaterialMeshes(modelPath); if (materialMeshes.empty()) { std::cerr << "No material meshes found in loaded model: " << modelPath << std::endl; - return; + return false; } - int entitiesCreated = 0; for (const auto& materialMesh : materialMeshes) { // Create an entity name based on model and material std::string entityName = modelName + "_Material_" + std::to_string(materialMesh.materialIndex) + @@ -249,14 +245,15 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, } } - entitiesCreated++; } else { std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; } } } catch (const std::exception& e) { std::cerr << "Error loading GLTF model: " << e.what() << std::endl; + return false; } + return true; } /** diff --git a/attachments/simple_engine/scene_loading.h b/attachments/simple_engine/scene_loading.h index 332fdfa2..dee4f0dc 100644 --- a/attachments/simple_engine/scene_loading.h +++ b/attachments/simple_engine/scene_loading.h @@ -17,7 +17,7 @@ class ModelLoader; * @param rotation The rotation to apply to the model. * @param scale The scale to apply to the model. */ -void LoadGLTFModel(Engine* engine, const std::string& modelPath, +bool LoadGLTFModel(Engine* engine, const std::string& modelPath, const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale); /** diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 5d3fd564..bbed1ae8 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -1,5 +1,3 @@ -// Combined vertex and fragment shader for PBR rendering - // Input from vertex buffer struct VSInput { [[vk::location(0)]] float3 Position; @@ -17,21 +15,20 @@ struct VSOutput { float4 Position : SV_POSITION; float3 WorldPos; float3 Normal : NORMAL; + float3 GeometricNormal : NORMAL1; float2 UV : TEXCOORD0; float4 Tangent : TANGENT; }; // Light data structure for storage buffer -// Explicit offsets ensure exact match with CPU-side layout struct LightData { - [[vk::offset(0)]] float4 position; // Directional: direction (w=0); Point/Spot/Emissive: world position (w=1) - [[vk::offset(16)]] float4 color; // Light color and intensity - // Match GLM column-major matrices in CPU - [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; // Light space matrix for shadow mapping - [[vk::offset(96)]] int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive - [[vk::offset(100)]] float range; // Light range - [[vk::offset(104)]] float innerConeAngle;// For spot lights - [[vk::offset(108)]] float outerConeAngle;// For spot lights + [[vk::offset(0)]] float4 position; + [[vk::offset(16)]] float4 color; + [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; + [[vk::offset(96)]] int lightType; + [[vk::offset(100)]] float range; + [[vk::offset(104)]] float innerConeAngle; + [[vk::offset(108)]] float outerConeAngle; }; // Uniform buffer (now without fixed light arrays) @@ -44,12 +41,16 @@ struct UniformBufferObject { float gamma; float prefilteredCubeMipLevels; float scaleIBLAmbient; - int lightCount; // Number of active lights (dynamic) - int padding0; // Padding for alignment - float padding1; // Padding for alignment - float padding2; // Padding for alignment + int lightCount; + int padding0; + float padding1; + float padding2; + float2 screenDimensions; }; + +[[vk::binding(0, 1)]] Sampler2D opaqueSceneColor; + // Push constants for material properties struct PushConstants { float4 baseColorFactor; @@ -62,12 +63,14 @@ struct PushConstants { int emissiveTextureSet; float alphaMask; float alphaMaskCutoff; - float3 emissiveFactor; // Emissive factor for HDR emissive sources - float emissiveStrength; // KHR_materials_emissive_strength extension - float transmissionFactor; // KHR_materials_transmission - int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness - float glossinessFactor; // SpecGloss glossiness scalar - float3 specularFactor; // SpecGloss specular color factor + float3 emissiveFactor; + float emissiveStrength; + float transmissionFactor; + int useSpecGlossWorkflow; + float glossinessFactor; + float3 specularFactor; + float ior; + bool hasEmissiveStrengthExt; }; // Constants @@ -80,7 +83,7 @@ static const float PI = 3.14159265359; [[vk::binding(3, 0)]] Sampler2D normalMap; [[vk::binding(4, 0)]] Sampler2D occlusionMap; [[vk::binding(5, 0)]] Sampler2D emissiveMap; -[[vk::binding(6, 0)]] StructuredBuffer lightBuffer; // Dynamic light storage buffer +[[vk::binding(6, 0)]] StructuredBuffer lightBuffer; [[vk::push_constant]] PushConstants material; @@ -89,26 +92,28 @@ float DistributionGGX(float NdotH, float roughness) { float a = roughness * roughness; float a2 = a * a; float NdotH2 = NdotH * NdotH; - float nom = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; - - return nom / denom; + return nom / max(denom, 0.000001); } float GeometrySmith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; - float ggx1 = NdotV / (NdotV * (1.0 - k) + k); float ggx2 = NdotL / (NdotL * (1.0 - k) + k); - return ggx1 * ggx2; } float3 FresnelSchlick(float cosTheta, float3 F0) { - return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); + return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); +} + +float3 Fresnel_Dielectric(float cosTheta, float ior) { + float r0 = (1.0 - ior) / (1.0 + ior); + float3 F0 = float3(r0 * r0); + return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); } // Vertex shader entry point @@ -116,239 +121,178 @@ float3 FresnelSchlick(float cosTheta, float3 F0) { VSOutput VSMain(VSInput input) { VSOutput output; - - // Use instance matrices directly float4x4 instanceModelMatrix = input.InstanceModelMatrix; float3x3 instanceNormalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; - - // Transform position to world space: entity model * instance model float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); - - // Pass world position to fragment shader output.WorldPos = worldPos.xyz; - - // Transform normal and tangent to world space (apply instance normal then entity model) float3x3 model3x3 = (float3x3)ubo.model; output.Normal = normalize(mul(model3x3, mul(instanceNormalMatrix3x3, input.Normal))); - + output.GeometricNormal = normalize(mul((float3x3)input.InstanceNormalMatrix, input.Normal)); + output.GeometricNormal = normalize(mul(model3x3, output.GeometricNormal)); + float3x3 instanceModelMatrix3x3 = (float3x3)instanceModelMatrix; + // Tangents should be transformed by the normal matrix (inverse-transpose), + // not the raw model matrix, to keep TBN correct under non-uniform scaling. float3 instTangent = mul(instanceNormalMatrix3x3, input.Tangent.xyz); float3 worldTangent = normalize(mul(model3x3, instTangent)); - - // Pass texture coordinates output.UV = input.UV; - - // Pass world-space tangent (preserve handedness in w) output.Tangent = float4(worldTangent, input.Tangent.w); - return output; } +namespace Hable_Filmic_Tonemapping { + static const float A = 0.15; static const float B = 0.50; + static const float C = 0.10; static const float D = 0.20; + static const float E = 0.02; static const float F = 0.30; + static const float W = 11.2; + float3 Uncharted2Tonemap(float3 x) { + return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; + } +} + // Fragment shader entry point [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET { - // Sample material textures (flip V to match glTF UV origin) + // --- 1. Material Properties --- float2 uv = float2(input.UV.x, 1.0 - input.UV.y); - float4 baseColor = baseColorMap.Sample(uv) * material.baseColorFactor; - // For MR workflow: metallic in B, roughness in G; For SpecGloss: RGB=specular color, A=glossiness + float4 baseColor = (material.baseColorTextureSet < 0) ? material.baseColorFactor : baseColorMap.Sample(uv) * material.baseColorFactor; float4 mrOrSpecGloss = (material.physicalDescriptorTextureSet < 0) ? float4(1.0, 1.0, 1.0, 1.0) : metallicRoughnessMap.Sample(uv); - float metallic = 0.0; - float roughness = 1.0; - float3 specColorSG = float3(0.0, 0.0, 0.0); + float metallic = 0.0, roughness = 1.0; + float3 F0, albedo; + if (material.useSpecGlossWorkflow != 0) { - // Specular-Glossiness workflow - specColorSG = mrOrSpecGloss.rgb * material.specularFactor; + float3 specColorSG = mrOrSpecGloss.rgb * material.specularFactor; float gloss = clamp(mrOrSpecGloss.a * material.glossinessFactor, 0.0, 1.0); roughness = clamp(1.0 - gloss, 0.0, 1.0); - metallic = 0.0; // not used in SpecGloss + F0 = specColorSG; + albedo = baseColor.rgb * (1.0 - max(F0.r, max(F0.g, F0.b))); } else { - // Metallic-Roughness workflow - float2 metallicRoughness = mrOrSpecGloss.bg; - metallic = clamp(metallicRoughness.x * material.metallicFactor, 0.0, 1.0); - roughness = clamp(metallicRoughness.y * material.roughnessFactor, 0.0, 1.0); + // glTF metallic-roughness texture packs metallic in B, roughness in G (linear space) + float metallicTex = mrOrSpecGloss.b; + float roughnessTex = mrOrSpecGloss.g; + metallic = clamp(metallicTex * material.metallicFactor, 0.0, 1.0); + roughness = clamp(roughnessTex * material.roughnessFactor, 0.0, 1.0); + F0 = lerp(float3(0.04, 0.04, 0.04), baseColor.rgb, metallic); + albedo = baseColor.rgb * (1.0 - metallic); } - float ao = occlusionMap.Sample(uv).r; + + float ao = (material.occlusionTextureSet < 0) ? 1.0 : occlusionMap.Sample(uv).r; + // Emissive: default to constant white when no emissive texture so authored emissiveFactor works per glTF spec. + // If a texture is present but factor is zero, assume (1,1,1) to preserve emissive textures by default. float3 emissiveTex = (material.emissiveTextureSet < 0) ? float3(1.0, 1.0, 1.0) : emissiveMap.Sample(uv).rgb; - float3 emissive = emissiveTex * material.emissiveFactor * material.emissiveStrength; + float3 emissiveFactor = material.emissiveFactor; + float3 emissive = emissiveTex * emissiveFactor; + if (material.hasEmissiveStrengthExt) + emissive *= material.emissiveStrength; - // Early alpha masking discard for masked materials only (glTF alphaMode == MASK) if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { discard; } - // Calculate normal in tangent space + // --- 2. Normal Calculation --- float3 N = normalize(input.Normal); if (material.normalTextureSet >= 0) { - // Apply normal mapping float3 tangentNormal = normalMap.Sample(uv).xyz * 2.0 - 1.0; - - float3 T = input.Tangent.xyz; - bool hasTangent = dot(T, T) > 1e-6; - if (hasTangent) { - // Orthonormalize T against N to reduce shading artifacts - T = normalize(T); - T = normalize(T - N * dot(N, T)); - } else { - // Fallback: derive tangent from screen-space derivatives of position and UVs - float3 dp1 = ddx(input.WorldPos); - float3 dp2 = ddy(input.WorldPos); - float2 duv1 = ddx(uv); - float2 duv2 = ddy(uv); - float det = duv1.x * duv2.y - duv1.y * duv2.x; - if (abs(det) > 1e-8) { - float r = 1.0 / det; - T = normalize((dp1 * duv2.y - dp2 * duv1.y) * r); - } else { - // Degenerate UV derivatives; fall back to a stable orthogonal vector - float3 up = (abs(N.y) < 0.999) ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); - T = normalize(cross(up, N)); - } - } - float handedness = hasTangent ? input.Tangent.w : 1.0; + float3 T = normalize(input.Tangent.xyz); + float handedness = input.Tangent.w; float3 B = normalize(cross(N, T)) * handedness; - // Construct tangent-to-world with T, B, N basis float3x3 TBN = float3x3(T, B, N); - // Transform from tangent to world space using column-vector convention N = normalize(mul(TBN, tangentNormal)); } - // Calculate view and reflection vectors float3 V = normalize(ubo.camPos.xyz - input.WorldPos); - float3 R = reflect(-V, N); - - // Calculate F0 (base reflectivity) - float3 F0; - if (material.useSpecGlossWorkflow != 0) { - // SpecGloss: use specular color directly - F0 = specColorSG; - } else { - // MR: interpolate between dielectric and baseColor by metallic - F0 = float3(0.04, 0.04, 0.04); - F0 = lerp(F0, baseColor.rgb, metallic); - } - - // Initialize lighting - float3 Lo = float3(0.0, 0.0, 0.0); - // Calculate lighting for each light (dynamic count - no limit) + // --- 3. Opaque Lighting Calculation --- + float3 diffuseLighting = float3(0.0,0.0,0.0), specularLighting = float3(0.0,0.0,0.0); for (int i = 0; i < ubo.lightCount; i++) { LightData light = lightBuffer[i]; - float3 lightVec = light.position.xyz; // w=0 indicates direction (directional), w=1 indicates position (point/spot/emissive) - float3 lightColor = light.color.rgb; - - // Handle emissive lights differently - if (light.lightType == 3) { // Emissive light - // Treat emissive like a positional contributor from its stored position - float3 L = normalize(lightVec - input.WorldPos); - float distance = length(lightVec - input.WorldPos); - float attenuation = 1.0 / (distance * distance); - float3 radiance = lightColor * attenuation; - + float3 L, radiance; + if (light.lightType == 1) { L = normalize(-light.position.xyz); radiance = light.color.rgb; } + else { L = normalize(light.position.xyz - input.WorldPos); float d = length(light.position.xyz - input.WorldPos); radiance = light.color.rgb / max(d * d, 0.001); } + float NdotL = max(dot(N, L), 0.0); + if (NdotL > 0.0) { float3 H = normalize(V + L); - - float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 0.0); float NdotH = max(dot(N, H), 0.0); float HdotV = max(dot(H, V), 0.0); - float D = DistributionGGX(NdotH, roughness); float G = GeometrySmith(NdotV, NdotL, roughness); float3 F = FresnelSchlick(HdotV, F0); - - float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; - float3 specular = numerator / denominator; - - float3 kS = F; - float3 kD = float3(1.0, 1.0, 1.0) - kS; - kD *= 1.0 - metallic; - - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; - - } else if (light.lightType == 1) { // Directional light - // For directional lights, position field stores direction; use no distance attenuation - float3 L = normalize(-lightVec); // light direction towards the surface - float3 radiance = lightColor; // No attenuation with distance - - float3 H = normalize(V + L); - - float NdotL = max(dot(N, L), 0.0); - float NdotV = max(dot(N, V), 0.0); - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); - - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmith(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(HdotV, F0); - - float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; - float3 specular = numerator / denominator; - - float3 kS = F; - float3 kD = float3(1.0, 1.0, 1.0) - kS; - kD *= 1.0 - metallic; - - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; - - } else { // Point/Spot lights - float3 L = normalize(lightVec - input.WorldPos); - float distance = length(lightVec - input.WorldPos); - float attenuation = 1.0 / (distance * distance); - float3 radiance = lightColor * attenuation; - - float3 H = normalize(V + L); - - float NdotL = max(dot(N, L), 0.0); - float NdotV = max(dot(N, V), 0.0); - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); - - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmith(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(HdotV, F0); - - float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; - float3 specular = numerator / denominator; - - float3 kS = F; - float3 kD = float3(1.0, 1.0, 1.0) - kS; - kD *= 1.0 - metallic; - - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 0.0001); + float3 kD = (1.0 - F) * (1.0 - metallic); + specularLighting += specular * radiance * NdotL; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL; } } + // specularLighting = min(specularLighting, 50.0); + float3 ambient = albedo * ao * (0.03 * ubo.scaleIBLAmbient); + float3 opaqueLit = diffuseLighting + specularLighting + ambient + emissive; - float3 ambient = baseColor.rgb * ao * (0.03 * ubo.scaleIBLAmbient); - // Base lit color from direct lighting and emissive - float3 opaqueLit = ambient + Lo + emissive; - - // Transmission respecting roughness and Fresnel, but without environment map + // --- 4. Final Color Assembly with Refraction --- + float3 color = opaqueLit; float T = clamp(material.transmissionFactor, 0.0, 1.0); - float NdotV_glass = max(dot(N, V), 0.0); - float3 Fv = FresnelSchlick(NdotV_glass, F0); - float Favg = (Fv.x + Fv.y + Fv.z) / 3.0; - float roughTrans = clamp(1.0 - (roughness * roughness), 0.0, 1.0); - float T_eff = T * (1.0 - Favg) * roughTrans; + float alphaOut = baseColor.a; - // Energy-conserving mix between opaque lighting and transmitted base color tint - float3 color = lerp(opaqueLit, baseColor.rgb, T_eff); + if (T > 0.0) { + // 1. FAKE REFRACTION (Screen-Space UV Distortion) + // Compute UV from clip-space with perspective divide -> NDC [-1,1] -> [0,1] + float2 ndc = input.Position.xy / max(input.Position.w, 1e-6); + float2 baseUV = saturate(ndc * 0.5 + 0.5); + float NdotV = max(dot(N, V), 0.0); + float refractionStrength = (1.0 - (1.0 / material.ior)) * 0.15; + float2 distortion = N.xy * Fresnel_Dielectric(NdotV, material.ior).r * refractionStrength; + float border = 1.0 / max(ubo.screenDimensions.x, ubo.screenDimensions.y); + float2 minUV = float2(border, border); + float2 maxUV = float2(1.0 - border, 1.0 - border); + float2 refrUV = clamp(baseUV + distortion, minUV, maxUV); + float3 refractedColor = opaqueSceneColor.Sample(refrUV).rgb; + + // Effective transmission depends solely on the transmission factor (KHR_materials_transmission) + // Do not multiply by baseColor.a, as many GLTF glass materials keep albedo alpha at 0 + float T_eff = saturate(T); + + refractedColor *= baseColor.rgb; + + // 3. Fresnel weighting for transmission: reduce diffuse with transmission; keep specular; add refraction + float3 F_view = FresnelSchlick(NdotV, F0); + float F_avg = (F_view.r + F_view.g + F_view.b) / 3.0; + + // Screen-space reflection (cheap): sample scene color using reflected view vector + float3 R = reflect(-V, N); + float2 reflOffset = R.xy * (0.08 * (1.0 - roughness)); + float2 reflUV = clamp(baseUV + reflOffset, minUV, maxUV); + float3 reflectedColor = opaqueSceneColor.Sample(reflUV).rgb; + float3 reflectWeight = F_view * (1.0 - roughness); + float metallicBoost = lerp(0.5, 1.2, metallic); + float3 reflectTerm = reflectedColor * reflectWeight * metallicBoost; + + float3 surfaceTerm = diffuseLighting * (1.0 - T_eff) + specularLighting + ambient + emissive + reflectTerm; + float3 refractedTerm = refractedColor * T_eff * (1.0 - F_avg); + color = surfaceTerm + refractedTerm; + + // 4. ALPHA: Correct glass transparency - higher transmission = lower alpha (more transparent) + float specAlpha = saturate(((F_view.r + F_view.g + F_view.b) / 3.0) * (1.0 - 0.2 * roughness)); + // For glass: base transparency from transmission, with specular reflection preserving some opacity + float baseTransparency = T_eff * (1.0 - F_avg * 0.5); + alphaOut = clamp(max(baseTransparency + specAlpha * (1.0 - T_eff), specAlpha * 0.1), 0.005, 1.0); + } - // Apply exposure before tone mapping for proper HDR workflow + // --- 5. Post-Processing --- + // Apply exposure and filmic tonemapping; apply gamma only if the swapchain is NOT sRGB color *= ubo.exposure; - // Standard Reinhard tone mapping - simple and effective - color = color / (1.0 + color); + // Uncharted2 / Hable filmic tonemap + float3 t = Hable_Filmic_Tonemapping::Uncharted2Tonemap(color * 1.2); + float3 w = Hable_Filmic_Tonemapping::Uncharted2Tonemap(float3(1,1,1) * (Hable_Filmic_Tonemapping::W / 1.2)); + color = t / max(w, float3(1e-6, 1e-6, 1e-6)); - // Gamma correction (convert from linear to sRGB) - color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); - - // Alpha approximates remaining opacity (higher transmission -> lower alpha), clamped for readability - float alphaOut = baseColor.a; - if (T > 0.001) { - alphaOut = clamp(1.0 - T_eff, 0.08, 0.60); + // ubo.padding0 repurposed as: outputIsSRGB (1 = swapchain is sRGB, 0 = UNORM) + if (ubo.padding0 == 0) { + color = pow(saturate(color), float3(1.0 / ubo.gamma)); + } else { + color = saturate(color); } + return float4(color, alphaOut); } diff --git a/attachments/simple_engine/thread_pool.h b/attachments/simple_engine/thread_pool.h new file mode 100644 index 00000000..6ae022ac --- /dev/null +++ b/attachments/simple_engine/thread_pool.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Generic reusable thread pool for background tasks (texture uploads, geometry processing, etc.) +class ThreadPool { +public: + explicit ThreadPool(size_t threadCount = std::thread::hardware_concurrency()) + : stopFlag(false) { + if (threadCount == 0) threadCount = 1; + for (size_t i = 0; i < threadCount; ++i) { + workers.emplace_back([this]() { this->workerLoop(); }); + } + } + + ~ThreadPool() { + shutdown(); + } + + template + auto enqueue(F&& f, Args&&... args) -> std::future::type> { + using return_type = typename std::invoke_result::type; + + auto task = std::make_shared>( + [func = std::decay_t(std::forward(f)), + tup = std::make_tuple(std::forward(args)...)]() mutable -> return_type { + return std::apply(std::move(func), std::move(tup)); + } + ); + + std::future res = task->get_future(); + { + std::unique_lock lock(queueMutex); + if (stopFlag) { + throw std::runtime_error("enqueue on stopped ThreadPool"); + } + tasks.emplace([task]() { (*task)(); }); + } + condVar.notify_one(); + return res; + } + + void shutdown() { + { + std::unique_lock lock(queueMutex); + if (stopFlag) return; + stopFlag = true; + } + condVar.notify_all(); + for (auto& t : workers) { + if (t.joinable()) t.join(); + } + workers.clear(); + } + +private: + void workerLoop() { + for (;;) { + std::function task; + { + std::unique_lock lock(queueMutex); + condVar.wait(lock, [this]() { return stopFlag || !tasks.empty(); }); + if (stopFlag && tasks.empty()) return; + task = std::move(tasks.front()); + tasks.pop(); + } + task(); + } + } + + std::vector workers; + std::queue> tasks; + std::mutex queueMutex; + std::condition_variable condVar; + std::atomic stopFlag; +}; diff --git a/attachments/simple_engine/vulkan_device.cpp b/attachments/simple_engine/vulkan_device.cpp index bd6d9136..72ca0171 100644 --- a/attachments/simple_engine/vulkan_device.cpp +++ b/attachments/simple_engine/vulkan_device.cpp @@ -73,12 +73,17 @@ bool VulkanDevice::pickPhysicalDevice() { } // Check for required features - auto features = device.template getFeatures2(); + auto features = device.template getFeatures2(); bool supportsRequiredFeatures = features.template get().dynamicRendering; if (!supportsRequiredFeatures) { std::cout << " - Does not support required features (dynamicRendering)" << std::endl; } + supportsRequiredFeatures &= features.template get().attachmentFeedbackLoopLayout; + if (!supportsRequiredFeatures) { + std::cout << " - Does not support required feature (attachmentFeedbackLoopLayout)" << std::endl; + } + return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && swapChainAdequate && supportsRequiredFeatures; }); @@ -124,6 +129,7 @@ bool VulkanDevice::createLogicalDevice(bool enableValidationLayers, const std::v // Enable required features auto features = physicalDevice.getFeatures2(); features.features.samplerAnisotropy = vk::True; + features.features.depthClamp = vk::True; // Enable Vulkan 1.3 features vk::PhysicalDeviceVulkan13Features vulkan13Features; @@ -131,6 +137,11 @@ bool VulkanDevice::createLogicalDevice(bool enableValidationLayers, const std::v vulkan13Features.synchronization2 = vk::True; features.pNext = &vulkan13Features; + vk::PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT feedbackLoopFeatures{}; + feedbackLoopFeatures.attachmentFeedbackLoopLayout = vk::True; + + vulkan13Features.pNext = &feedbackLoopFeatures; + // Create device vk::DeviceCreateInfo createInfo{ .pNext = &features,

<%^IN)<^y*PqWzgHrEuc`(S99W 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%0AH3!_B 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Ξ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 xs~p|0k+w8 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
M$goUS_!neNmRc?OlcgQGVRXl(rBv)O1 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

r5%nynNv7m%`!*9C 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 B6YKy0 literal 0 HcmV?d00001 diff --git a/images/rendering_pipeline_flowchart.svg b/images/rendering_pipeline_flowchart.svg deleted file mode 100644 index fa18ff39..00000000 --- a/images/rendering_pipeline_flowchart.svg +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - Vulkan Rendering Pipeline - - - - Scene Data - - - - Scene Culling - Determine visible objects - - - - Visible Objects - - - - Render Pass Management - Organize rendering passes - - - - Rendergraph - Manage dependencies - - - - Synchronization - Barriers, semaphores - - - - Command Generation - Create GPU commands - - - - Command Buffers - - - - Execution - Submit to GPU queue - - - - Post-Processing - Apply visual effects - - - - Deferred - - - Forward+ - - - PBR - - - - Final Image - - - - - - - - - - - - - - - - - - - - - - - - - Legend - - - - Pipeline Stage - - - - Component - - - - Technique - - - - Resource - From ea39e6588dde846d6200e693ec05b7e937e82c84 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 30 Jul 2025 23:32:05 -0700 Subject: [PATCH 038/102] address comments --- .../02_math_foundations.adoc | 59 ++++----- .../GUI/03_input_handling.adoc | 4 +- images/matrix-order-comparison.svg | 112 ++++++++++++++++++ 3 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 images/matrix-order-comparison.svg diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc index 9ae66c24..11b0c2b8 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc @@ -1,10 +1,9 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Mathematical Foundations :doctype: book :sectnums: :sectnumlevels: 4 -:toc: left :icons: font :source-highlighter: highlightjs :source-language: c++ @@ -97,34 +96,6 @@ glm::vec3 crossProduct = glm::cross(a, b); // (-3.0, 6.0, -3.0) glm::vec3 normalized = glm::normalize(a); // (0.267, 0.535, 0.802) ---- -==== The Right-Hand Rule - -The right-hand rule is a convention used in 3D graphics and mathematics to determine the orientation of coordinate systems and the direction of cross products. - -* *For Cross Products*: When calculating A × B: - 1. Point your right hand's index finger in the direction of vector A - 2. Point your middle finger in the direction of vector B (perpendicular to A) - 3. Your thumb now points in the direction of the resulting cross product - -* *For Coordinate Systems*: In a right-handed coordinate system: - 1. Point your right hand's index finger along the positive X-axis - 2. Point your middle finger along the positive Y-axis - 3. Your thumb points along the positive Z-axis - -[source,cpp] ----- -// The cross product direction follows the right-hand rule -glm::vec3 xAxis(1.0f, 0.0f, 0.0f); // Point right (positive X) -glm::vec3 yAxis(0.0f, 1.0f, 0.0f); // Point up (positive Y) - -// Cross product gives the Z axis in a right-handed system -glm::vec3 zAxis = glm::cross(xAxis, yAxis); // Points forward (positive Z) -// zAxis will be (0.0f, 0.0f, 1.0f) - -// If we reverse the order, we get the opposite direction -glm::vec3 negativeZ = glm::cross(yAxis, xAxis); // Points backward (negative Z) -// negativeZ will be (0.0f, 0.0f, -1.0f) ----- === Matrices and Transformations @@ -177,9 +148,31 @@ glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix; ==== Matrix Order Matters -The order of matrix multiplication is crucial: -* In `A * B`, the transformation B is applied first, then A -* For our camera: `projectionMatrix * viewMatrix * modelMatrix * vertex` +The order of matrix multiplication is crucial because transformations are applied from right to left. Getting the order wrong can completely change your object's final position and orientation. + +Consider this practical example: if you want to rotate a cube around its own center and then move it to a new position, you must apply the transformations in the correct order: + +[source,cpp] +---- +// CORRECT: Scale first, then rotate, then translate +// This rotates the cube around its own center, then moves it +glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix; + +// WRONG: Translate first, then rotate +// This would move the cube away from origin, then rotate it around the world origin +// The cube would orbit around the world center instead of rotating in place! +glm::mat4 wrongMatrix = rotationMatrix * translationMatrix * scaleMatrix; +---- + +For our camera pipeline: `projectionMatrix * viewMatrix * modelMatrix * vertex` +Each transformation prepares the data for the next stage, and changing this order would break the rendering pipeline. + +==== Visual Example: Why Matrix Order Matters + +The following diagram illustrates the difference between correct and incorrect matrix multiplication order when transforming a cube: + +.Matrix Transformation Order Comparison +image::../../../images/matrix-order-comparison.svg[Matrix Order Comparison showing correct T×R×S vs incorrect R×T×S transformation sequences] ==== Row-Major vs Column-Major Representation diff --git a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc index e367a9a1..313a8cae 100644 --- a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc +++ b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc @@ -156,9 +156,9 @@ void processInput(float deltaTime) { } ---- -=== Platform-Specific Input Implementation +=== Implementing Platform Adapters for Input -Our platform-agnostic input system needs specific implementations for each windowing library to capture input events. Here's an example implementation using GLFW, a popular windowing library: +While our input system design is platform-agnostic, we still need platform-specific adapters to bridge between our unified interface and each windowing library's native input events. Here's an example implementation using GLFW, a popular windowing library: ==== Example: GLFW Implementation diff --git a/images/matrix-order-comparison.svg b/images/matrix-order-comparison.svg new file mode 100644 index 00000000..68c26ef0 --- /dev/null +++ b/images/matrix-order-comparison.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + Matrix Order Comparison: T × R × S vs R × T × S + + + CORRECT ORDER: T × R × S (Applied right-to-left: Scale → Rotate → Translate) + + + + Step 1: Scale + + Original cube + at origin + + + + + + + + Step 2: Rotate + + Rotated in place + around its center + + + + + + + + Step 3: Translate + + Moved to new position + (final desired result) + + + Origin + + + + + + + INCORRECT ORDER: R × T × S (Applied right-to-left: Scale → Translate → Rotate) + + + + Step 1: Scale + + Original cube + at origin + + + + + + + + Step 2: Translate + + Moved away from + origin first + + + Origin + + + + + + + + Step 3: Rotate + + Rotated around world origin + (cube orbits, not desired!) + + + Origin + + + + + + + + Key Insight: + In the incorrect order, the cube is moved away from the origin first, then rotated around the world origin, + causing it to orbit in a circle rather than rotate in place and then move to the desired position. + + From 858b7924755bf6d315cb8f66bffc854a37cd91ea Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 30 Jul 2025 23:40:32 -0700 Subject: [PATCH 039/102] address comments --- en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc index ac33e5ce..ebafec83 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc @@ -21,7 +21,7 @@ Throughout this chapter, we've delved into several key aspects of engine archite 2. *Component Systems* - We implemented a flexible component-based architecture that allows for modular and reusable code. This approach promotes composition over inheritance, making it easier to create diverse game objects without deep inheritance hierarchies. -3. *Resource Management* - We designed a robust resource management system that efficiently handles assets like textures, meshes, and shaders. Our system includes features like reference counting, caching, and hot reloading to optimize memory usage and improve development workflow. +3. *Resource Management* - We designed a robust resource management system that efficiently handles assets like textures, meshes, and shaders. Our system includes features such as reference counting, caching, and hot reloading to optimize memory usage and improve development workflow. 4. *Rendering Pipeline* - We structured a flexible rendering pipeline that can accommodate different rendering techniques and effects. Our pipeline includes stages for scene culling, render pass management, command generation, and post-processing. From 815aa7921425f592e6a0e32642379212f69d3e6c Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 31 Jul 2025 10:52:37 -0700 Subject: [PATCH 040/102] Need to investigate a few things, (physics isn't colliding against the geometry). However, this is probably good for a first version to PR. --- attachments/simple_engine/audio_system.cpp | 40 +- attachments/simple_engine/crash_reporter.h | 3 - attachments/simple_engine/debug_system.h | 1 - attachments/simple_engine/engine.cpp | 388 ++++++++++--- attachments/simple_engine/engine.h | 89 ++- attachments/simple_engine/imgui_system.cpp | 39 ++ attachments/simple_engine/imgui_system.h | 16 + attachments/simple_engine/main.cpp | 4 +- attachments/simple_engine/mesh_component.cpp | 67 +++ attachments/simple_engine/mesh_component.h | 8 + attachments/simple_engine/physics_system.cpp | 526 +++++++++++------- attachments/simple_engine/physics_system.h | 63 ++- attachments/simple_engine/platform.cpp | 11 + attachments/simple_engine/platform.h | 18 + attachments/simple_engine/renderer.h | 1 + .../simple_engine/renderer_compute.cpp | 2 +- attachments/simple_engine/renderer_core.cpp | 13 +- .../simple_engine/renderer_rendering.cpp | 8 + .../simple_engine/renderer_resources.cpp | 8 + attachments/simple_engine/scene_loading.cpp | 27 +- .../simple_engine/shaders/physics.slang | 93 +++- 21 files changed, 1054 insertions(+), 371 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 714a3047..936ab00a 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -439,7 +439,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { // Upload audio data to OpenAL buffer alBufferData(buffer, format, pcmBuffer.data(), - samplesProcessed * sizeof(int16_t), sampleRate); + static_cast(samplesProcessed * sizeof(int16_t)), static_cast(sampleRate)); CheckOpenALError("alBufferData"); // Queue the buffer @@ -793,10 +793,9 @@ AudioSource* AudioSystem::CreateAudioSource(const std::string& name) { } // Store the source - AudioSource* sourcePtr = source.get(); sources.push_back(std::move(source)); - return sourcePtr; + return sources.back().get(); } AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { @@ -804,7 +803,7 @@ AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { auto source = std::make_unique(name); // Set up debug ping parameters - // The ping will cycle every 1.5 seconds (0.5s ping + 1.0s silence) + // The ping will cycle every 1.5 seconds (0.5 s ping + 1.0 s silence) constexpr float sampleRate = 44100.0f; constexpr float pingDuration = 0.5f; constexpr float silenceDuration = 1.0f; @@ -814,20 +813,19 @@ AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { source->SetAudioLength(totalCycleSamples); // Store the source - AudioSource* sourcePtr = source.get(); sources.push_back(std::move(source)); - return sourcePtr; + return sources.back().get(); } -void AudioSystem::SetListenerPosition(float x, float y, float z) { +void AudioSystem::SetListenerPosition(const float x, const float y, const float z) { listenerPosition[0] = x; listenerPosition[1] = y; listenerPosition[2] = z; } -void AudioSystem::SetListenerOrientation(float forwardX, float forwardY, float forwardZ, - float upX, float upY, float upZ) { +void AudioSystem::SetListenerOrientation(const float forwardX, const float forwardY, const float forwardZ, + const float upX, const float upY, const float upZ) { listenerOrientation[0] = forwardX; listenerOrientation[1] = forwardY; listenerOrientation[2] = forwardZ; @@ -836,17 +834,17 @@ void AudioSystem::SetListenerOrientation(float forwardX, float forwardY, float f listenerOrientation[5] = upZ; } -void AudioSystem::SetListenerVelocity(float x, float y, float z) { +void AudioSystem::SetListenerVelocity(const float x, const float y, const float z) { listenerVelocity[0] = x; listenerVelocity[1] = y; listenerVelocity[2] = z; } -void AudioSystem::SetMasterVolume(float volume) { +void AudioSystem::SetMasterVolume(const float volume) { masterVolume = volume; } -void AudioSystem::EnableHRTF(bool enable) { +void AudioSystem::EnableHRTF(const bool enable) { hrtfEnabled = enable; } @@ -854,7 +852,7 @@ bool AudioSystem::IsHRTFEnabled() const { return hrtfEnabled; } -void AudioSystem::SetHRTFCPUOnly(bool cpuOnly) { +void AudioSystem::SetHRTFCPUOnly(const bool cpuOnly) { hrtfCPUOnly = cpuOnly; } @@ -868,15 +866,12 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { constexpr uint32_t hrtfSampleCount = 256; // Number of samples per impulse response constexpr uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) constexpr uint32_t channelCount = 2; // Stereo (left and right ears) - const float sampleRate = 44100.0f; // Sample rate for HRTF data - const float speedOfSound = 343.0f; // Speed of sound in m/s - const float headRadius = 0.0875f; // Average head radius in meters + // const float headRadius = 0.0875f; // Average head radius in meters - // Try to load from file first (only if filename is provided) + // Try to load from a file first (only if the filename is provided) if (!filename.empty()) { - std::ifstream file(filename, std::ios::binary); - if (file.is_open()) { - // Read file header to determine format + if (std::ifstream file(filename, std::ios::binary); file.is_open()) { + // Read the file header to determine a format char header[4]; file.read(header, 4); @@ -889,7 +884,7 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { if (fileChannelCount == channelCount) { hrtfData.resize(fileHrtfSize * filePositionCount * fileChannelCount); - file.read(reinterpret_cast(hrtfData.data()), hrtfData.size() * sizeof(float)); + file.read(reinterpret_cast(hrtfData.data()), static_cast(hrtfData.size() * sizeof(float))); hrtfSize = fileHrtfSize; numHrtfPositions = filePositionCount; @@ -921,6 +916,8 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { float z = std::cos(elevation) * std::cos(azimuth); for (uint32_t channel = 0; channel < channelCount; channel++) { + constexpr float speedOfSound = 343.0f; + constexpr float sampleRate = 44100.0f; // Calculate ear position (left ear: -0.1m, right ear: +0.1m on x-axis) float earX = (channel == 0) ? -0.1f : 0.1f; @@ -944,7 +941,6 @@ bool AudioSystem::LoadHRTFData(const std::string& filename) { // Generate impulse response - uint32_t samplesGenerated = 0; for (uint32_t i = 0; i < hrtfSampleCount; i++) { float value = 0.0f; diff --git a/attachments/simple_engine/crash_reporter.h b/attachments/simple_engine/crash_reporter.h index 82f1b356..12c7ab4f 100644 --- a/attachments/simple_engine/crash_reporter.h +++ b/attachments/simple_engine/crash_reporter.h @@ -1,15 +1,12 @@ #pragma once #include -#include #include #include -#include #include #include #include #include -#include #include #include diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h index 3f3a0e55..2f6ae385 100644 --- a/attachments/simple_engine/debug_system.h +++ b/attachments/simple_engine/debug_system.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 54a81f02..74d9b858 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -1,10 +1,12 @@ #include "engine.h" #include "scene_loading.h" +#include "mesh_component.h" #include #include #include #include +#include // This implementation corresponds to the Engine_Architecture chapter in the tutorial: // @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -44,12 +46,12 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); if (!imguiWantsMouse) { - // Handle mouse click for poke functionality (right mouse button) + // Handle mouse click for ball throwing (right mouse button) if (buttons & 2) { // Right mouse button (bit 1) if (!cameraControl.mouseRightPressed) { cameraControl.mouseRightPressed = true; - // Perform poke on mouse click - HandleMousePoke(x, y); + // Throw a ball on mouse click + ThrowBall(x, y); } } else { cameraControl.mouseRightPressed = false; @@ -159,6 +161,10 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Initialize a physics system physicsSystem->SetRenderer(renderer.get()); + + // Enable GPU acceleration for physics calculations to drastically speed up computations + physicsSystem->SetGPUAccelerationEnabled(true); + if (!physicsSystem->Initialize()) { return false; } @@ -171,6 +177,13 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Connect ImGui system to an audio system for UI controls imguiSystem->SetAudioSystem(audioSystem.get()); + // Generate ball material properties once at load time + GenerateBallMaterial(); + + // Initialize physics scaling system + InitializePhysicsScaling(); + + initialized = true; return true; #endif @@ -184,7 +197,9 @@ void Engine::Run() { running = true; // Main loop + int loopCount = 0; while (running) { + loopCount++; // Process platform events if (!platform->ProcessEvents()) { running = false; @@ -194,6 +209,25 @@ void Engine::Run() { // Calculate delta time deltaTime = CalculateDeltaTime(); + // Update frame counter and FPS + frameCount++; + fpsUpdateTimer += deltaTime; + + // Update window title with FPS and frame count every second + if (fpsUpdateTimer >= 1.0f) { + uint64_t framesSinceLastUpdate = frameCount - lastFPSUpdateFrame; + currentFPS = framesSinceLastUpdate / fpsUpdateTimer; + + // Update window title with frame count and FPS + std::string title = "Simple Engine - Frame: " + std::to_string(frameCount) + + " | FPS: " + std::to_string(static_cast(currentFPS)); + platform->SetWindowTitle(title); + + // Reset timer and frame counter for next update + fpsUpdateTimer = 0.0f; + lastFPSUpdateFrame = frameCount; + } + // Update Update(deltaTime); @@ -327,7 +361,19 @@ ImGuiSystem* Engine::GetImGuiSystem() const { } void Engine::Update(float deltaTime) { - // Update a physics system + // Debug: Verify Update method is being called + static int updateCallCount = 0; + updateCallCount++; + // Process pending ball creations (outside rendering loop to avoid memory pool constraints) + ProcessPendingBalls(); + + + if (activeCamera) { + glm::vec3 currentCameraPosition = activeCamera->GetPosition(); + physicsSystem->SetCameraPosition(currentCameraPosition); + } + + // Use real deltaTime for physics to maintain proper timing physicsSystem->Update(deltaTime); // Update audio system @@ -418,6 +464,37 @@ void Engine::UpdateCameraControls(float deltaTime) const { auto* cameraTransform = activeCamera->GetOwner()->GetComponent(); if (!cameraTransform) return; + // Check if camera tracking is enabled + if (imguiSystem && imguiSystem->IsCameraTrackingEnabled()) { + // Find the first active ball entity + Entity* ballEntity = nullptr; + for (const auto& entity : entities) { + if (entity->IsActive() && entity->GetName().find("Ball_") != std::string::npos) { + ballEntity = entity.get(); + break; + } + } + + if (ballEntity) { + // Get ball's transform component + auto* ballTransform = ballEntity->GetComponent(); + if (ballTransform) { + glm::vec3 ballPosition = ballTransform->GetPosition(); + + // Position camera at a fixed offset from the ball for good viewing + glm::vec3 cameraOffset = glm::vec3(2.0f, 1.5f, 2.0f); // Behind and above the ball + glm::vec3 cameraPosition = ballPosition + cameraOffset; + + // Update camera position and target + cameraTransform->SetPosition(cameraPosition); + activeCamera->SetTarget(ballPosition); + + return; // Skip manual controls when tracking + } + } + } + + // Manual camera controls (only when tracking is disabled) // Calculate movement speed float velocity = cameraControl.cameraSpeed * deltaTime; @@ -463,75 +540,88 @@ void Engine::UpdateCameraControls(float deltaTime) const { activeCamera->SetTarget(target); } -void Engine::HandleMousePoke(float mouseX, float mouseY) const { - if (!activeCamera || !physicsSystem) { - return; - } +void Engine::GenerateBallMaterial() { + // Generate 8 random material properties for PBR + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dis(0.0f, 1.0f); - // Get window dimensions - int windowWidth, windowHeight; - platform->GetWindowSize(&windowWidth, &windowHeight); + // Generate bright, vibrant albedo colors for better visibility + std::uniform_real_distribution brightDis(0.6f, 1.0f); // Ensure bright colors + ballMaterial.albedo = glm::vec3(brightDis(gen), brightDis(gen), brightDis(gen)); - // Convert mouse coordinates to normalized device coordinates (-1 to 1) - float ndcX = (2.0f * mouseX) / static_cast(windowWidth) - 1.0f; - float ndcY = 1.0f - (2.0f * mouseY) / static_cast(windowHeight); + // Random metallic value (0.0 to 1.0) + ballMaterial.metallic = dis(gen); - // Get camera matrices - glm::mat4 viewMatrix = activeCamera->GetViewMatrix(); - glm::mat4 projMatrix = activeCamera->GetProjectionMatrix(); + // Random roughness value (0.0 to 1.0) + ballMaterial.roughness = dis(gen); - // Calculate inverse matrices - glm::mat4 invView = glm::inverse(viewMatrix); - glm::mat4 invProj = glm::inverse(projMatrix); + // Random ambient occlusion (typically 0.8 to 1.0 for good lighting) + ballMaterial.ao = 0.8f + dis(gen) * 0.2f; - // Convert NDC to world space - glm::vec4 rayClip = glm::vec4(ndcX, ndcY, -1.0f, 1.0f); - glm::vec4 rayEye = invProj * rayClip; - rayEye = glm::vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); - glm::vec4 rayWorld = invView * rayEye; + // Random emissive color (usually subtle) + ballMaterial.emissive = glm::vec3(dis(gen) * 0.3f, dis(gen) * 0.3f, dis(gen) * 0.3f); - // Get ray origin and direction - glm::vec3 rayOrigin = activeCamera->GetPosition(); - glm::vec3 rayDirection = glm::normalize(glm::vec3(rayWorld)); + // Very low bounciness (0.05 to 0.15 for 85-95% momentum loss per bounce) + ballMaterial.bounciness = 0.05f + dis(gen) * 0.1f; - // Perform raycast - glm::vec3 hitPosition; - glm::vec3 hitNormal; - Entity* hitEntity = nullptr; + std::cout << "Generated ball material - Albedo: (" << ballMaterial.albedo.x << ", " + << ballMaterial.albedo.y << ", " << ballMaterial.albedo.z << "), " + << "Metallic: " << ballMaterial.metallic << ", Roughness: " << ballMaterial.roughness + << ", Bounciness: " << ballMaterial.bounciness << std::endl; +} - if (physicsSystem->Raycast(rayOrigin, rayDirection, 1000.0f, &hitPosition, &hitNormal, &hitEntity)) { - if (hitEntity) { - std::cout << "Mouse poke hit entity: " << hitEntity->GetName() << std::endl; +void Engine::InitializePhysicsScaling() { + // Based on issue analysis: balls reaching 120+ m/s and extreme positions like (-244, -360, -244) + // The previous 200.0f force scale was causing supersonic speeds and balls flying out of scene + // Need much more conservative scaling for realistic visual gameplay + + // Use smaller game unit scale for more controlled physics + physicsScaling.gameUnitsToMeters = 0.1f; // 1 game unit = 0.1 meter (10cm) - smaller scale + + // Much reduced force scaling to prevent extreme speeds + // With base forces 0.01f-0.05f, this gives final forces of 0.001f-0.005f + physicsScaling.forceScale = 1.0f; // Minimal force scaling for realistic movement + physicsScaling.physicsTimeScale = 1.0f; // Keep time scale normal + physicsScaling.gravityScale = 1.0f; // Keep gravity proportional to scale + + std::cout << "Physics scaling initialized:" << std::endl; + std::cout << " Game units to meters: " << physicsScaling.gameUnitsToMeters << std::endl; + std::cout << " Force scale: " << physicsScaling.forceScale << std::endl; + std::cout << " Time scale: " << physicsScaling.physicsTimeScale << std::endl; + std::cout << " Gravity scale: " << physicsScaling.gravityScale << std::endl; + + // Apply scaled gravity to physics system + glm::vec3 realWorldGravity(0.0f, -9.81f, 0.0f); + glm::vec3 scaledGravity = ScaleGravityForPhysics(realWorldGravity); + physicsSystem->SetGravity(scaledGravity); +} - // Find or create rigid body for the entity - RigidBody* rigidBody = nullptr; - // Check if entity already has a rigid body (this is a simplified approach) - // In a real implementation, you'd have a component system to track this - rigidBody = physicsSystem->CreateRigidBody(hitEntity, CollisionShape::Box, 1.0f); +float Engine::ScaleForceForPhysics(float gameForce) const { + // Scale force based on the relationship between game units and real world + // and the force scaling factor to make physics feel right + return gameForce * physicsScaling.forceScale * physicsScaling.gameUnitsToMeters; +} - if (rigidBody) { - // Apply a small impulse in the direction of the ray - glm::vec3 impulse = rayDirection * 0.5f; // Small force magnitude as requested - rigidBody->ApplyImpulse(impulse, glm::vec3(0.0f)); +glm::vec3 Engine::ScaleGravityForPhysics(const glm::vec3& realWorldGravity) const { + // Scale gravity based on game unit scale and gravity scaling factor + // If 1 game unit = 1 meter, then gravity should remain -9.81 + // If 1 game unit = 0.1 meter, then gravity should be -0.981 + return realWorldGravity * physicsScaling.gravityScale * physicsScaling.gameUnitsToMeters; +} - std::cout << "Applied poke impulse to " << hitEntity->GetName() << std::endl; - } - } - } else { - std::cout << "Mouse poke missed - no entity hit" << std::endl; - } +float Engine::ScaleTimeForPhysics(float deltaTime) const { + // Scale time for physics simulation if needed + // This can be used to slow down or speed up physics relative to rendering + return deltaTime * physicsScaling.physicsTimeScale; } -void Engine::HandleMouseHover(float mouseX, float mouseY) { +void Engine::ThrowBall(float mouseX, float mouseY) { if (!activeCamera || !physicsSystem) { return; } - // Update current mouse position - currentMouseX = mouseX; - currentMouseY = mouseY; - // Get window dimensions int windowWidth, windowHeight; platform->GetWindowSize(&windowWidth, &windowHeight); @@ -548,52 +638,142 @@ void Engine::HandleMouseHover(float mouseX, float mouseY) { glm::mat4 invView = glm::inverse(viewMatrix); glm::mat4 invProj = glm::inverse(projMatrix); - // Convert NDC to world space + // Convert NDC to world space for direction glm::vec4 rayClip = glm::vec4(ndcX, ndcY, -1.0f, 1.0f); glm::vec4 rayEye = invProj * rayClip; rayEye = glm::vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); glm::vec4 rayWorld = invView * rayEye; - // Get ray origin and direction - glm::vec3 rayOrigin = activeCamera->GetPosition(); - glm::vec3 rayDirection = glm::normalize(glm::vec3(rayWorld)); - - // Perform raycast - glm::vec3 hitPosition; - glm::vec3 hitNormal; - Entity* hitEntity = nullptr; - - if (physicsSystem->Raycast(rayOrigin, rayDirection, 1000.0f, &hitPosition, &hitNormal, &hitEntity)) { - if (hitEntity) { - // Check if this entity is pokeable (has "_SMALL_POKEABLE" suffix) - std::string entityName = hitEntity->GetName(); - - if (entityName.find("_SMALL_POKEABLE") != std::string::npos) { - // Update a hovered entity if it's different from the current one - if (hoveredEntity != hitEntity) { - hoveredEntity = hitEntity; - renderer->SetHighlightedEntity(hoveredEntity); - std::cout << "Now hovering over pokeable entity: " << entityName << std::endl; - } - } else { - // Clear hover if we're over a non-pokeable entity - if (hoveredEntity != nullptr) { - std::cout << "No longer hovering over pokeable entity" << std::endl; - hoveredEntity = nullptr; - renderer->SetHighlightedEntity(nullptr); - } - } + // Calculate screen center in world coordinates + // Screen center is at NDC (0, 0) which corresponds to the center of the view + glm::vec4 screenCenterClip = glm::vec4(0.0f, 0.0f, -1.0f, 1.0f); + glm::vec4 screenCenterEye = invProj * screenCenterClip; + screenCenterEye = glm::vec4(screenCenterEye.x, screenCenterEye.y, -1.0f, 0.0f); + glm::vec4 screenCenterWorld = invView * screenCenterEye; + glm::vec3 screenCenterDirection = glm::normalize(glm::vec3(screenCenterWorld)); + + // Calculate world position for screen center at a reasonable distance from camera + glm::vec3 cameraPosition = activeCamera->GetPosition(); + glm::vec3 screenCenterWorldPos = cameraPosition + screenCenterDirection * 2.0f; // 2 units in front of camera + + // Calculate throw direction from screen center toward mouse position + glm::vec3 throwDirection = glm::normalize(glm::vec3(rayWorld)); + + // Add upward component for realistic arc trajectory + throwDirection.y += 0.3f; // Add upward bias for throwing arc + throwDirection = glm::normalize(throwDirection); // Re-normalize after modification + + // Generate ball properties now + static int ballCounter = 0; + std::string ballName = "Ball_" + std::to_string(ballCounter++); + + std::random_device rd; + std::mt19937 gen(rd()); + + // Launch balls from screen center toward mouse cursor + glm::vec3 spawnPosition = screenCenterWorldPos; + + // Add small random variation to avoid identical paths + std::uniform_real_distribution posDis(-0.1f, 0.1f); + spawnPosition.x += posDis(gen); + spawnPosition.y += posDis(gen); + spawnPosition.z += posDis(gen); + + // Log camera, screen center, and spawn positions for debugging + std::cout << "CAMERA POSITION: (" << cameraPosition.x << ", " << cameraPosition.y << ", " << cameraPosition.z << ")" << std::endl; + std::cout << "SCREEN CENTER WORLD POS: (" << screenCenterWorldPos.x << ", " << screenCenterWorldPos.y << ", " << screenCenterWorldPos.z << ")" << std::endl; + std::cout << "BALL SPAWN POSITION: (" << spawnPosition.x << ", " << spawnPosition.y << ", " << spawnPosition.z << ")" << std::endl; + std::cout << "THROW DIRECTION: (" << throwDirection.x << ", " << throwDirection.y << ", " << throwDirection.z << ")" << std::endl; + std::cout << "MOUSE NDC: (" << ndcX << ", " << ndcY << ")" << std::endl; + std::uniform_real_distribution spinDis(-10.0f, 10.0f); + std::uniform_real_distribution forceDis(15.0f, 35.0f); // Stronger force range for proper throwing feel + + // Store ball creation data for processing outside rendering loop + PendingBall pendingBall; + pendingBall.spawnPosition = spawnPosition; + pendingBall.throwDirection = throwDirection; // This is now the corrected direction toward geometry + pendingBall.throwForce = ScaleForceForPhysics(forceDis(gen)); // Apply physics scaling to force + pendingBall.randomSpin = glm::vec3(spinDis(gen), spinDis(gen), spinDis(gen)); + pendingBall.ballName = ballName; + + pendingBalls.push_back(pendingBall); +} + +void Engine::ProcessPendingBalls() { + if (pendingBalls.empty()) { + return; + } + + // Process all pending balls + for (const auto& pendingBall : pendingBalls) { + // Create ball entity + Entity* ballEntity = CreateEntity(pendingBall.ballName); + if (!ballEntity) { + std::cerr << "Failed to create ball entity: " << pendingBall.ballName << std::endl; + continue; + } + + // Add transform component + auto* transform = ballEntity->AddComponent(); + if (!transform) { + std::cerr << "Failed to add TransformComponent to ball: " << pendingBall.ballName << std::endl; + continue; + } + transform->SetPosition(pendingBall.spawnPosition); + transform->SetScale(glm::vec3(1.0f)); // Tennis ball size scale + + // Add mesh component with sphere geometry + auto* mesh = ballEntity->AddComponent(); + if (!mesh) { + std::cerr << "Failed to add MeshComponent to ball: " << pendingBall.ballName << std::endl; + continue; } - } else { - // Clear hover if no entity is hit - if (hoveredEntity != nullptr) { - std::cout << "No longer hovering over pokeable entity" << std::endl; - hoveredEntity = nullptr; - renderer->SetHighlightedEntity(nullptr); + // Create tennis ball-sized, bright red sphere + glm::vec3 brightRed(1.0f, 0.0f, 0.0f); + mesh->CreateSphere(0.0335f, brightRed, 32); // Tennis ball radius, bright color, high detail + mesh->SetTexturePath(renderer->SHARED_BRIGHT_RED_ID); // Use bright red texture for visibility + + // Verify mesh geometry was created + const auto& vertices = mesh->GetVertices(); + const auto& indices = mesh->GetIndices(); + if (vertices.empty() || indices.empty()) { + std::cerr << "ERROR: CreateSphere failed to generate geometry!" << std::endl; + continue; + } + + // Pre-allocate Vulkan resources for this entity (now outside rendering loop) + if (!renderer->preAllocateEntityResources(ballEntity)) { + std::cerr << "Failed to pre-allocate resources for ball: " << pendingBall.ballName << std::endl; + continue; + } + + // Create rigid body with sphere collision shape + RigidBody* rigidBody = physicsSystem->CreateRigidBody(ballEntity, CollisionShape::Sphere, 1.0f); + if (rigidBody) { + // Set bounciness from material + rigidBody->SetRestitution(ballMaterial.bounciness); + + // Apply throw force and spin + glm::vec3 throwImpulse = pendingBall.throwDirection * pendingBall.throwForce; + rigidBody->ApplyImpulse(throwImpulse, glm::vec3(0.0f)); + rigidBody->SetAngularVelocity(pendingBall.randomSpin); + + std::cout << "Ball " << pendingBall.ballName << " created successfully with force " + << pendingBall.throwForce << std::endl; } } + + // Clear processed balls + pendingBalls.clear(); +} + +void Engine::HandleMouseHover(float mouseX, float mouseY) { + // Update current mouse position for any systems that might need it + currentMouseX = mouseX; + currentMouseY = mouseY; } + #if PLATFORM_ANDROID // Android-specific implementation bool Engine::InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers) { @@ -610,6 +790,22 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo // Set mouse callback platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { + // Check if ImGui wants to capture mouse input first + bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + + if (!imguiWantsMouse) { + // Handle mouse click for ball throwing (right mouse button) + if (buttons & 2) { // Right mouse button (bit 1) + if (!cameraControl.mouseRightPressed) { + cameraControl.mouseRightPressed = true; + // Throw a ball on mouse click + ThrowBall(x, y); + } + } else { + cameraControl.mouseRightPressed = false; + } + } + if (imguiSystem) { imguiSystem->HandleMouse(x, y, buttons); } @@ -650,6 +846,10 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo // Initialize physics system physicsSystem->SetRenderer(renderer.get()); + + // Enable GPU acceleration for physics calculations to drastically speed up computations + physicsSystem->SetGPUAccelerationEnabled(true); + if (!physicsSystem->Initialize()) { return false; } @@ -666,6 +866,12 @@ bool Engine::InitializeAndroid(android_app* app, const std::string& appName, boo // Connect ImGui system to audio system for UI controls imguiSystem->SetAudioSystem(audioSystem.get()); + // Generate ball material properties once at load time + GenerateBallMaterial(); + + // Initialize physics scaling system + InitializePhysicsScaling(); + initialized = true; return true; } diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 25887865..897dc758 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -182,6 +182,12 @@ class Engine { float deltaTime = 0.0f; uint64_t lastFrameTime = 0; + // Frame counter and FPS calculation + uint64_t frameCount = 0; + float fpsUpdateTimer = 0.0f; + float currentFPS = 0.0f; + uint64_t lastFPSUpdateFrame = 0; + // Camera control state struct CameraControlState { bool moveForward = false; @@ -201,11 +207,46 @@ class Engine { float mouseSensitivity = 0.1f; } cameraControl; - // Hover state tracking - Entity* hoveredEntity = nullptr; + // Mouse position tracking float currentMouseX = 0.0f; float currentMouseY = 0.0f; + // Ball material properties for PBR + struct BallMaterial { + glm::vec3 albedo; + float metallic; + float roughness; + float ao; + glm::vec3 emissive; + float bounciness; + }; + + BallMaterial ballMaterial; + + // Physics scaling configuration + // The bistro scene spans roughly 20 game units and represents a realistic cafe/bistro space + // Based on issue feedback: game units should NOT equal 1m and need proper scaling + // Analysis shows bistro geometry pieces are much smaller than assumed + struct PhysicsScaling { + float gameUnitsToMeters = 0.1f; // 1 game unit = 0.1 meter (10cm) - more realistic scale + float physicsTimeScale = 1.0f; // Normal time scale for stable physics + float forceScale = 2.0f; // Much reduced force scaling for visual gameplay (was 10.0f) + float gravityScale = 0.1f; // Scaled gravity for smaller world scale + }; + + PhysicsScaling physicsScaling; + + // Pending ball creation data (to avoid memory pool constraints during rendering) + struct PendingBall { + glm::vec3 spawnPosition; + glm::vec3 throwDirection; + float throwForce; + glm::vec3 randomSpin; + std::string ballName; + }; + + std::vector pendingBalls; + /** * @brief Update the engine state. * @param deltaTime The time elapsed since the last update. @@ -237,14 +278,52 @@ class Engine { void UpdateCameraControls(float deltaTime) const; /** - * @brief Handle mouse poke interaction to apply forces to clicked objects. + * @brief Generate random PBR material properties for the ball. + */ + void GenerateBallMaterial(); + + /** + * @brief Initialize physics scaling based on scene analysis. + */ + void InitializePhysicsScaling(); + + + /** + * @brief Convert a force value from game units to physics units. + * @param gameForce Force in game units. + * @return Force scaled for physics simulation. + */ + float ScaleForceForPhysics(float gameForce) const; + + /** + * @brief Convert gravity from real-world units to game physics units. + * @param realWorldGravity Gravity in m/s². + * @return Gravity scaled for game physics. + */ + glm::vec3 ScaleGravityForPhysics(const glm::vec3& realWorldGravity) const; + + /** + * @brief Convert time delta for physics simulation. + * @param deltaTime Real delta time. + * @return Scaled delta time for physics. + */ + float ScaleTimeForPhysics(float deltaTime) const; + + /** + * @brief Throw a ball into the scene with random properties. * @param mouseX The x-coordinate of the mouse click. * @param mouseY The y-coordinate of the mouse click. */ - void HandleMousePoke(float mouseX, float mouseY) const; + void ThrowBall(float mouseX, float mouseY); + + + /** + * @brief Process pending ball creations outside the rendering loop. + */ + void ProcessPendingBalls(); /** - * @brief Handle mouse hover detection for highlighting pokeable entities. + * @brief Handle mouse hover to track current mouse position. * @param mouseX The x-coordinate of the mouse position. * @param mouseY The y-coordinate of the mouse position. */ diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index f87d4aa6..ffda91ff 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -319,6 +319,45 @@ void ImGuiSystem::NewFrame() { ImGui::Text("HRTF Processing: DISABLED"); } + // Ball Debugging Controls + ImGui::Separator(); + ImGui::Text("Ball Debugging Controls:"); + + if (ImGui::Checkbox("Ball-Only Rendering", &ballOnlyRenderingEnabled)) { + std::cout << "Ball-only rendering " << (ballOnlyRenderingEnabled ? "enabled" : "disabled") << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("?##BallOnlyHelp")) { + // Help tooltip will be shown on hover + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, only balls will be rendered.\nAll other geometry (bistro scene) will be hidden."); + } + + if (ImGui::Checkbox("Camera Track Ball", &cameraTrackingEnabled)) { + std::cout << "Camera tracking " << (cameraTrackingEnabled ? "enabled" : "disabled") << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("?##CameraTrackHelp")) { + // Help tooltip will be shown on hover + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, camera will automatically\nfollow and look at the ball."); + } + + // Status display + if (ballOnlyRenderingEnabled) { + ImGui::Text("Status: Only balls are being rendered"); + } else { + ImGui::Text("Status: All geometry is being rendered"); + } + + if (cameraTrackingEnabled) { + ImGui::Text("Camera: Tracking ball automatically"); + } else { + ImGui::Text("Camera: Manual control (WASD + mouse)"); + } + ImGui::End(); } diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index 80dbb268..3dba6830 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -107,6 +107,18 @@ class ImGuiSystem { */ bool IsPBREnabled() const { return pbrEnabled; } + /** + * @brief Get the current ball-only rendering state. + * @return True if ball-only rendering is enabled, false otherwise. + */ + bool IsBallOnlyRenderingEnabled() const { return ballOnlyRenderingEnabled; } + + /** + * @brief Get the current camera tracking state. + * @return True if camera tracking is enabled, false otherwise. + */ + bool IsCameraTrackingEnabled() const { return cameraTrackingEnabled; } + private: // ImGui context ImGuiContext* context = nullptr; @@ -156,6 +168,10 @@ class ImGuiSystem { // PBR rendering state bool pbrEnabled = true; + // Ball-only rendering and camera tracking state + bool ballOnlyRenderingEnabled = false; + bool cameraTrackingEnabled = false; + /** * @brief Create Vulkan resources for ImGui. * @return True if creation was successful, false otherwise. diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index 2b57b64b..238618da 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -35,9 +35,7 @@ void SetupScene(Engine* engine) { engine->SetActiveCamera(camera); // Load GLTF model synchronously on the main thread - std::cout << "Loading GLTF model synchronously..." << std::endl; - LoadGLTFModel(engine, "../Assets/bistro_gltf/bistro.gltf"); - std::cout << "GLTF model loading completed." << std::endl; + LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); } #if PLATFORM_ANDROID diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index 08f3e6a1..af9bdc0f 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -1,5 +1,6 @@ #include "mesh_component.h" #include "model_loader.h" +#include // Most of the MeshComponent class implementation is in the header file // This file is mainly for any methods that might need additional implementation @@ -82,6 +83,72 @@ void MeshComponent::CreateCube(float size, const glm::vec3& color) { }; } +void MeshComponent::CreateSphere(float radius, const glm::vec3& color, int segments) { + vertices.clear(); + indices.clear(); + + // Generate sphere vertices using parametric equations + for (int lat = 0; lat <= segments; ++lat) { + float theta = lat * M_PI / segments; // Latitude angle (0 to PI) + float sinTheta = sin(theta); + float cosTheta = cos(theta); + + for (int lon = 0; lon <= segments; ++lon) { + float phi = lon * 2.0f * M_PI / segments; // Longitude angle (0 to 2*PI) + float sinPhi = sin(phi); + float cosPhi = cos(phi); + + // Calculate position + glm::vec3 position = { + radius * sinTheta * cosPhi, + radius * cosTheta, + radius * sinTheta * sinPhi + }; + + // Normal is the same as normalized position for a sphere centered at origin + glm::vec3 normal = glm::normalize(position); + + // Texture coordinates + glm::vec2 texCoord = { + (float)lon / segments, + (float)lat / segments + }; + + // Calculate tangent (derivative with respect to longitude) + glm::vec3 tangent = { + -sinTheta * sinPhi, + 0.0f, + sinTheta * cosPhi + }; + tangent = glm::normalize(tangent); + + vertices.push_back({ + position, + normal, + texCoord, + glm::vec4(tangent, 1.0f) + }); + } + } + + // Generate indices for triangles + for (int lat = 0; lat < segments; ++lat) { + for (int lon = 0; lon < segments; ++lon) { + int current = lat * (segments + 1) + lon; + int next = current + segments + 1; + + // Create two triangles for each quad + indices.push_back(current); + indices.push_back(next); + indices.push_back(current + 1); + + indices.push_back(current + 1); + indices.push_back(next); + indices.push_back(next + 1); + } + } +} + void MeshComponent::LoadFromModel(const Model* model) { if (!model) { return; diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index cdeb7196..d73b9724 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -170,6 +170,14 @@ class MeshComponent : public Component { */ void CreateCube(float size = 1.0f, const glm::vec3& color = glm::vec3(1.0f)); + /** + * @brief Create a simple sphere mesh. + * @param radius The radius of the sphere. + * @param color The color of the sphere. + * @param segments The number of segments (resolution). + */ + void CreateSphere(float radius = 1.0f, const glm::vec3& color = glm::vec3(1.0f), int segments = 16); + /** * @brief Load mesh data from a Model. * @param model Pointer to the model to load from. diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index 61cd78f6..5d1e5dac 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -1,11 +1,16 @@ #include "physics_system.h" #include "entity.h" #include "renderer.h" +#include "transform_component.h" #include + #include #include #include #include +#include +#include + // Concrete implementation of RigidBody class ConcreteRigidBody : public RigidBody { @@ -14,10 +19,18 @@ class ConcreteRigidBody : public RigidBody { : entity(entity), shape(shape), mass(mass) { // Initialize with entity's transform if available if (entity) { - // This would normally get the position, rotation, and scale from the entity's transform component - position = glm::vec3(0.0f); - rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion - scale = glm::vec3(1.0f); + // Get the position, rotation, and scale from the entity's transform component + auto* transform = entity->GetComponent(); + if (transform) { + position = transform->GetPosition(); + rotation = glm::quat(transform->GetRotation()); // Convert from Euler angles to quaternion + scale = transform->GetScale(); + } else { + // Fallback to defaults if no transform component + position = glm::vec3(0.0f); + rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + scale = glm::vec3(1.0f); + } } } @@ -25,58 +38,62 @@ class ConcreteRigidBody : public RigidBody { void SetPosition(const glm::vec3& position) override { this->position = position; - std::cout << "Setting rigid body position to (" << position.x << ", " << position.y << ", " << position.z << ")" << std::endl; + + // Update entity transform component for visual representation + if (entity) { + auto* transform = entity->GetComponent(); + if (transform) { + transform->SetPosition(position); + } + } } void SetRotation(const glm::quat& rotation) override { this->rotation = rotation; - std::cout << "Setting rigid body rotation" << std::endl; + + // Update entity transform component for visual representation + if (entity) { + auto* transform = entity->GetComponent(); + if (transform) { + // Convert quaternion to Euler angles for the transform component + glm::vec3 eulerAngles = glm::eulerAngles(rotation); + transform->SetRotation(eulerAngles); + } + } } void SetScale(const glm::vec3& scale) override { this->scale = scale; - std::cout << "Setting rigid body scale to (" << scale.x << ", " << scale.y << ", " << scale.z << ")" << std::endl; } void SetMass(float mass) override { this->mass = mass; - std::cout << "Setting rigid body mass to " << mass << std::endl; } void SetRestitution(float restitution) override { this->restitution = restitution; - std::cout << "Setting rigid body restitution to " << restitution << std::endl; } void SetFriction(float friction) override { this->friction = friction; - std::cout << "Setting rigid body friction to " << friction << std::endl; } void ApplyForce(const glm::vec3& force, const glm::vec3& localPosition) override { - std::cout << "Applying force (" << force.x << ", " << force.y << ", " << force.z << ") " - << "at local position (" << localPosition.x << ", " << localPosition.y << ", " << localPosition.z << ")" << std::endl; - // In a real implementation, this would apply the force to the rigid body linearVelocity += force / mass; } void ApplyImpulse(const glm::vec3& impulse, const glm::vec3& localPosition) override { - std::cout << "Applying impulse (" << impulse.x << ", " << impulse.y << ", " << impulse.z << ") " - << "at local position (" << localPosition.x << ", " << localPosition.y << ", " << localPosition.z << ")" << std::endl; - // In a real implementation, this would apply the impulse to the rigid body linearVelocity += impulse / mass; } void SetLinearVelocity(const glm::vec3& velocity) override { linearVelocity = velocity; - std::cout << "Setting rigid body linear velocity to (" << velocity.x << ", " << velocity.y << ", " << velocity.z << ")" << std::endl; } void SetAngularVelocity(const glm::vec3& velocity) override { angularVelocity = velocity; - std::cout << "Setting rigid body angular velocity to (" << velocity.x << ", " << velocity.y << ", " << velocity.z << ")" << std::endl; } [[nodiscard]] glm::vec3 GetPosition() const override { @@ -96,8 +113,12 @@ class ConcreteRigidBody : public RigidBody { } void SetKinematic(bool kinematic) override { + // Prevent balls from being set as kinematic - they should always be dynamic + if (entity && entity->GetName().find("Ball_") == 0 && kinematic) { + return; + } + this->kinematic = kinematic; - std::cout << "Setting rigid body kinematic to " << (kinematic ? "true" : "false") << std::endl; } [[nodiscard]] bool IsKinematic() const override { @@ -128,27 +149,8 @@ class ConcreteRigidBody : public RigidBody { return friction; } - void Update(const float deltaTime, const glm::vec3& gravity) { - if (kinematic) { - return; - } - - // Apply gravity - linearVelocity += gravity * deltaTime; - // Update position - position += linearVelocity * deltaTime; - // Update rotation - const glm::quat angularVelocityQuat(0.0f, angularVelocity.x, angularVelocity.y, angularVelocity.z); - rotation += 0.5f * deltaTime * angularVelocityQuat * rotation; - rotation = glm::normalize(rotation); - - // Update entity transform if available - if (entity) { - // This would normally set the position, rotation, and scale on the entity's transform component - } - } private: Entity* entity = nullptr; @@ -166,6 +168,7 @@ class ConcreteRigidBody : public RigidBody { float friction = 0.5f; bool kinematic = false; + bool markedForRemoval = false; // Flag to mark physics body for removal friend class PhysicsSystem; }; @@ -182,8 +185,6 @@ bool PhysicsSystem::Initialize() { // This is a placeholder implementation // In a real implementation, this would initialize the physics engine - std::cout << "Initializing physics system" << std::endl; - // Initialize Vulkan resources if GPU acceleration is enabled and the renderer is set if (gpuAccelerationEnabled && renderer) { if (!InitializeVulkanResources()) { @@ -197,20 +198,28 @@ bool PhysicsSystem::Initialize() { } void PhysicsSystem::Update(float deltaTime) { - // If GPU acceleration is enabled, and we have a renderer, use the GPU + // GPU-ONLY physics - NO CPU fallback available + + // Check if GPU physics is properly initialized and available if (initialized && gpuAccelerationEnabled && renderer && rigidBodies.size() <= maxGPUObjects) { + // Debug: Log that we're using GPU physics + static bool gpuPhysicsLogged = false; + if (!gpuPhysicsLogged) { + gpuPhysicsLogged = true; + } SimulatePhysicsOnGPU(deltaTime); } else { - // Fall back to CPU physics - // Update all rigid bodies - for (auto& rigidBody : rigidBodies) { - auto concreteRigidBody = dynamic_cast(rigidBody.get()); - concreteRigidBody->Update(deltaTime, gravity); + // NO CPU FALLBACK - GPU physics must work or physics is disabled + static bool noFallbackLogged = false; + if (!noFallbackLogged) { + noFallbackLogged = true; } - // Perform collision detection and resolution - // This would be a complex algorithm in a real implementation } + + + // Clean up rigid bodies marked for removal (happens regardless of GPU/CPU physics path) + CleanupMarkedBodies(); } RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, float mass) { @@ -220,7 +229,6 @@ RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, // Store the rigid body rigidBodies.push_back(std::move(rigidBody)); - std::cout << "Rigid body created for entity: " << (entity ? entity->GetName() : "null") << std::endl; return rigidBodies.back().get(); } @@ -235,7 +243,6 @@ bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { // Remove the rigid body rigidBodies.erase(it); - std::cout << "Rigid body removed" << std::endl; return true; } @@ -245,8 +252,6 @@ bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { void PhysicsSystem::SetGravity(const glm::vec3& gravity) { this->gravity = gravity; - - std::cout << "Setting gravity to (" << gravity.x << ", " << gravity.y << ", " << gravity.z << ")" << std::endl; } glm::vec3 PhysicsSystem::GetGravity() const { @@ -415,29 +420,58 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, break; } case CollisionShape::Mesh: { - // Mesh intersection test - // In a real implementation, this would perform intersection tests with each triangle in the mesh - // For simplicity, we'll just simulate a hit with a sphere + // Proper mesh intersection test using triangle data + auto* meshComponent = entity->GetComponent(); + if (meshComponent) { + const auto& vertices = meshComponent->GetVertices(); + const auto& indices = meshComponent->GetIndices(); + + // Test intersection with each triangle in the mesh + for (size_t i = 0; i < indices.size(); i += 3) { + if (i + 2 >= indices.size()) break; + + // Get triangle vertices + glm::vec3 v0 = vertices[indices[i]].position; + glm::vec3 v1 = vertices[indices[i + 1]].position; + glm::vec3 v2 = vertices[indices[i + 2]].position; + + // Transform vertices to world space + auto* transform = entity->GetComponent(); + if (transform) { + glm::mat4 transformMatrix = transform->GetModelMatrix(); + v0 = glm::vec3(transformMatrix * glm::vec4(v0, 1.0f)); + v1 = glm::vec3(transformMatrix * glm::vec4(v1, 1.0f)); + v2 = glm::vec3(transformMatrix * glm::vec4(v2, 1.0f)); + } - float radius = 0.5f; // Default radius + // Ray-triangle intersection using Möller-Trumbore algorithm + glm::vec3 edge1 = v1 - v0; + glm::vec3 edge2 = v2 - v0; + glm::vec3 h = glm::cross(normalizedDirection, edge2); + float a = glm::dot(edge1, h); - // Calculate coefficients for quadratic equation - glm::vec3 oc = origin - position; - float a = glm::dot(normalizedDirection, normalizedDirection); - float b = 2.0f * glm::dot(oc, normalizedDirection); - float c = glm::dot(oc, oc) - radius * radius; - float discriminant = b * b - 4 * a * c; + if (a > -0.00001f && a < 0.00001f) continue; // Ray parallel to triangle - if (discriminant >= 0) { - // Calculate intersection distance - float t = (-b - std::sqrt(discriminant)) / (2.0f * a); + float f = 1.0f / a; + glm::vec3 s = origin - v0; + float u = f * glm::dot(s, h); - // Check if the intersection is within range - if (t > 0 && t < closestHitDistance) { - hitDistance = t; - localHitPosition = origin + normalizedDirection * t; - localHitNormal = glm::normalize(localHitPosition - position); - hit = true; + if (u < 0.0f || u > 1.0f) continue; + + glm::vec3 q = glm::cross(s, edge1); + float v = f * glm::dot(normalizedDirection, q); + + if (v < 0.0f || u + v > 1.0f) continue; + + float t = f * glm::dot(edge2, q); + + if (t > 0.00001f && t < closestHitDistance) { + hitDistance = t; + localHitPosition = origin + normalizedDirection * t; + localHitNormal = glm::normalize(glm::cross(edge1, edge2)); + hit = true; + closestHitDistance = t; // Update for closer triangles + } } } break; @@ -469,11 +503,6 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, if (hitEntity) { *hitEntity = closestHitEntity; } - - std::cout << "Hit found at distance " << closestHitDistance << std::endl; - std::cout << "Hit position: (" << closestHitPosition.x << ", " << closestHitPosition.y << ", " << closestHitPosition.z << ")" << std::endl; - std::cout << "Hit normal: (" << closestHitNormal.x << ", " << closestHitNormal.y << ", " << closestHitNormal.z << ")" << std::endl; - std::cout << "Hit entity: " << (closestHitEntity ? closestHitEntity->GetName() : "null") << std::endl; } return hitFound; @@ -537,7 +566,7 @@ bool PhysicsSystem::InitializeVulkanResources() { std::vector resolveShaderCode = readFile("shaders/physics.spv"); vulkanResources.resolveShaderModule = createShaderModule(raiiDevice, resolveShaderCode); - // Create descriptor set layout + // Create a descriptor set layout std::array bindings = { // Physics data buffer vk::DescriptorSetLayoutBinding( @@ -742,22 +771,35 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create params buffer: " + std::string(e.what())); } - // Initialize counter-buffer + // Create persistent mapped memory pointers for improved performance + try { + // Map physics buffer memory persistently + vulkanResources.persistentPhysicsMemory = vulkanResources.physicsBufferMemory.mapMemory(0, physicsBufferSize); + + // Map counter buffer memory persistently + vulkanResources.persistentCounterMemory = vulkanResources.counterBufferMemory.mapMemory(0, counterBufferSize); + + // Map params buffer memory persistently + vulkanResources.persistentParamsMemory = vulkanResources.paramsBufferMemory.mapMemory(0, paramsBufferSize); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to create persistent mapped memory: " + std::string(e.what())); + } + + // Initialize counter-buffer using persistent memory uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count - void* data = vulkanResources.counterBufferMemory.mapMemory(0, sizeof(initialCounters)); - memcpy(data, initialCounters, sizeof(initialCounters)); - vulkanResources.counterBufferMemory.unmapMemory(); + memcpy(vulkanResources.persistentCounterMemory, initialCounters, sizeof(initialCounters)); - // Create a descriptor pool + // Create a descriptor pool with capacity for 4 physics stages std::array poolSizes = { - vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 4), - vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1) + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 16), // 4 storage buffers × 4 stages + vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 4) // 1 uniform buffer × 4 stages }; vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; poolInfo.poolSizeCount = static_cast(poolSizes.size()); poolInfo.pPoolSizes = poolSizes.data(); - poolInfo.maxSets = 1; + poolInfo.maxSets = 4; // Support 4 descriptor sets for 4 physics stages vulkanResources.descriptorPool = vk::raii::DescriptorPool(raiiDevice, poolInfo); // Allocate descriptor sets @@ -768,11 +810,7 @@ bool PhysicsSystem::InitializeVulkanResources() { descriptorSetAllocInfo.pSetLayouts = &descriptorSetLayoutRef; try { - std::vector raiiDescriptorSets = raiiDevice.allocateDescriptorSets(descriptorSetAllocInfo); - vulkanResources.descriptorSets.resize(raiiDescriptorSets.size()); - for (size_t i = 0; i < raiiDescriptorSets.size(); ++i) { - vulkanResources.descriptorSets[i] = *raiiDescriptorSets[i]; - } + vulkanResources.descriptorSets = raiiDevice.allocateDescriptorSets(descriptorSetAllocInfo); } catch (const std::exception& e) { throw std::runtime_error("Failed to allocate descriptor sets: " + std::string(e.what())); } @@ -801,12 +839,12 @@ bool PhysicsSystem::InitializeVulkanResources() { vk::DescriptorBufferInfo paramsBufferInfo; paramsBufferInfo.buffer = *vulkanResources.paramsBuffer; paramsBufferInfo.offset = 0; - paramsBufferInfo.range = paramsBufferSize; + paramsBufferInfo.range = VK_WHOLE_SIZE; // Use VK_WHOLE_SIZE to ensure entire buffer is accessible std::array descriptorWrites; // Physics buffer - descriptorWrites[0].setDstSet(vulkanResources.descriptorSets[0]) + descriptorWrites[0].setDstSet(*vulkanResources.descriptorSets[0]) .setDstBinding(0) .setDstArrayElement(0) .setDescriptorCount(1) @@ -814,7 +852,7 @@ bool PhysicsSystem::InitializeVulkanResources() { .setPBufferInfo(&physicsBufferInfo); // Collision buffer - descriptorWrites[1].setDstSet(vulkanResources.descriptorSets[0]) + descriptorWrites[1].setDstSet(*vulkanResources.descriptorSets[0]) .setDstBinding(1) .setDstArrayElement(0) .setDescriptorCount(1) @@ -822,7 +860,7 @@ bool PhysicsSystem::InitializeVulkanResources() { .setPBufferInfo(&collisionBufferInfo); // Pair buffer - descriptorWrites[2].setDstSet(vulkanResources.descriptorSets[0]) + descriptorWrites[2].setDstSet(*vulkanResources.descriptorSets[0]) .setDstBinding(2) .setDstArrayElement(0) .setDescriptorCount(1) @@ -830,7 +868,7 @@ bool PhysicsSystem::InitializeVulkanResources() { .setPBufferInfo(&pairBufferInfo); // Counter buffer - descriptorWrites[3].setDstSet(vulkanResources.descriptorSets[0]) + descriptorWrites[3].setDstSet(*vulkanResources.descriptorSets[0]) .setDstBinding(3) .setDstArrayElement(0) .setDescriptorCount(1) @@ -838,7 +876,7 @@ bool PhysicsSystem::InitializeVulkanResources() { .setPBufferInfo(&counterBufferInfo); // Params buffer - descriptorWrites[4].setDstSet(vulkanResources.descriptorSets[0]) + descriptorWrites[4].setDstSet(*vulkanResources.descriptorSets[0]) .setDstBinding(4) .setDstArrayElement(0) .setDescriptorCount(1) @@ -866,6 +904,10 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to allocate command buffer: " + std::string(e.what())); } + // Create a dedicated fence for compute synchronization + vk::FenceCreateInfo fenceInfo{}; + vulkanResources.computeFence = vk::raii::Fence(raiiDevice, fenceInfo); + return true; } catch (const std::exception& e) { std::cerr << "Error initializing Vulkan resources: " << e.what() << std::endl; @@ -882,10 +924,53 @@ void PhysicsSystem::CleanupVulkanResources() { // Wait for the device to be idle before cleaning up renderer->WaitIdle(); - // With RAII, we just need to set the resources to nullptr - // The destructors will handle the cleanup + // Cleanup in proper order to avoid validation errors + // 1. Clear descriptor sets BEFORE destroying the descriptor pool + vulkanResources.descriptorSets.clear(); + + // 2. Destroy pipelines before pipeline layout + vulkanResources.resolvePipeline = nullptr; + vulkanResources.narrowPhasePipeline = nullptr; + vulkanResources.broadPhasePipeline = nullptr; + vulkanResources.integratePipeline = nullptr; + + // 3. Destroy pipeline layout before descriptor set layout + vulkanResources.pipelineLayout = nullptr; + vulkanResources.descriptorSetLayout = nullptr; + + // 4. Destroy shader modules + vulkanResources.resolveShaderModule = nullptr; + vulkanResources.narrowPhaseShaderModule = nullptr; + vulkanResources.broadPhaseShaderModule = nullptr; + vulkanResources.integrateShaderModule = nullptr; + + // 5. Destroy descriptor pool after descriptor sets are cleared + vulkanResources.descriptorPool = nullptr; + + // 6. Destroy command buffer before command pool vulkanResources.commandBuffer = nullptr; vulkanResources.commandPool = nullptr; + + // 7. Destroy compute fence + vulkanResources.computeFence = nullptr; + + // 8. Unmap persistent memory pointers before destroying buffer memory + if (vulkanResources.persistentPhysicsMemory && *vulkanResources.physicsBufferMemory) { + vulkanResources.physicsBufferMemory.unmapMemory(); + vulkanResources.persistentPhysicsMemory = nullptr; + } + + if (vulkanResources.persistentCounterMemory && *vulkanResources.counterBufferMemory) { + vulkanResources.counterBufferMemory.unmapMemory(); + vulkanResources.persistentCounterMemory = nullptr; + } + + if (vulkanResources.persistentParamsMemory && *vulkanResources.paramsBufferMemory) { + vulkanResources.paramsBufferMemory.unmapMemory(); + vulkanResources.persistentParamsMemory = nullptr; + } + + // 8. Destroy buffers and their memory vulkanResources.paramsBuffer = nullptr; vulkanResources.paramsBufferMemory = nullptr; vulkanResources.counterBuffer = nullptr; @@ -896,83 +981,99 @@ void PhysicsSystem::CleanupVulkanResources() { vulkanResources.collisionBufferMemory = nullptr; vulkanResources.physicsBuffer = nullptr; vulkanResources.physicsBufferMemory = nullptr; - vulkanResources.descriptorPool = nullptr; - vulkanResources.resolvePipeline = nullptr; - vulkanResources.narrowPhasePipeline = nullptr; - vulkanResources.broadPhasePipeline = nullptr; - vulkanResources.integratePipeline = nullptr; - vulkanResources.pipelineLayout = nullptr; - vulkanResources.descriptorSetLayout = nullptr; - vulkanResources.resolveShaderModule = nullptr; - vulkanResources.narrowPhaseShaderModule = nullptr; - vulkanResources.broadPhaseShaderModule = nullptr; - vulkanResources.integrateShaderModule = nullptr; } -void PhysicsSystem::UpdateGPUPhysicsData() const { +void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { if (!renderer) { return; } - // Validate Vulkan resources before using them + // Validate Vulkan resources and persistent memory pointers before using them if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.counterBufferMemory == VK_NULL_HANDLE || - *vulkanResources.paramsBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBufferMemory == VK_NULL_HANDLE) { - std::cerr << "PhysicsSystem::UpdateGPUPhysicsData: Invalid Vulkan resources" << std::endl; + *vulkanResources.paramsBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBufferMemory == VK_NULL_HANDLE || + !vulkanResources.persistentPhysicsMemory || !vulkanResources.persistentCounterMemory || !vulkanResources.persistentParamsMemory) { + std::cerr << "PhysicsSystem::UpdateGPUPhysicsData: Invalid Vulkan resources or persistent memory pointers" << std::endl; return; } - // Map the physics buffer - void* data = vulkanResources.physicsBufferMemory.mapMemory(0, sizeof(GPUPhysicsData) * rigidBodies.size()); - - // Copy physics data to the buffer - auto* gpuData = static_cast(data); - for (size_t i = 0; i < rigidBodies.size(); i++) { - const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); - - gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); - gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, - concreteRigidBody->GetRotation().z, concreteRigidBody->GetRotation().w); - gpuData[i].linearVelocity = glm::vec4(concreteRigidBody->GetLinearVelocity(), concreteRigidBody->GetRestitution()); - gpuData[i].angularVelocity = glm::vec4(concreteRigidBody->GetAngularVelocity(), concreteRigidBody->GetFriction()); - gpuData[i].force = glm::vec4(glm::vec3(0.0f), concreteRigidBody->IsKinematic() ? 1.0f : 0.0f); - gpuData[i].torque = glm::vec4(glm::vec3(0.0f), 1.0f); // Always use gravity - - // Set collider data based on a collider type - switch (concreteRigidBody->GetShape()) { - case CollisionShape::Sphere: - gpuData[i].colliderData = glm::vec4(0.5f, 0.0f, 0.0f, static_cast(0)); // 0 = Sphere - gpuData[i].colliderData2 = glm::vec4(0.0f); - break; - case CollisionShape::Box: - gpuData[i].colliderData = glm::vec4(0.5f, 0.5f, 0.5f, static_cast(1)); // 1 = Box - gpuData[i].colliderData2 = glm::vec4(0.0f); - break; - default: - gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); // Invalid - gpuData[i].colliderData2 = glm::vec4(0.0f); - break; + // Skip physics buffer operations if no rigid bodies exist + if (!rigidBodies.empty()) { + // Use persistent mapped memory for physics buffer + auto* gpuData = static_cast(vulkanResources.persistentPhysicsMemory); + for (size_t i = 0; i < rigidBodies.size(); i++) { + const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); + + gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); + gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, + concreteRigidBody->GetRotation().z, concreteRigidBody->GetRotation().w); + gpuData[i].linearVelocity = glm::vec4(concreteRigidBody->GetLinearVelocity(), concreteRigidBody->GetRestitution()); + gpuData[i].angularVelocity = glm::vec4(concreteRigidBody->GetAngularVelocity(), concreteRigidBody->GetFriction()); + // CRITICAL FIX: Initialize forces properly instead of always resetting to zero + // For balls, we want to start with zero force and let the shader apply gravity + // For static geometry, forces should remain zero + auto initialForce = glm::vec3(0.0f); + auto initialTorque = glm::vec3(0.0f); + + // For dynamic bodies (balls), allow forces to be applied by the shader + // The shader will add gravity and other forces each frame + gpuData[i].force = glm::vec4(initialForce, concreteRigidBody->IsKinematic() ? 1.0f : 0.0f); + gpuData[i].torque = glm::vec4(initialTorque, 1.0f); // Always use gravity + + // Set collider data based on a collider type + switch (concreteRigidBody->GetShape()) { + case CollisionShape::Sphere: + // Use tennis ball radius (0.0335f) instead of hardcoded 0.5f + gpuData[i].colliderData = glm::vec4(0.0335f, 0.0f, 0.0f, static_cast(0)); // 0 = Sphere + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + case CollisionShape::Box: + gpuData[i].colliderData = glm::vec4(0.5f, 0.5f, 0.5f, static_cast(1)); // 1 = Box + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + case CollisionShape::Mesh: + // Represent mesh as a large bounding box for GPU collision detection + // This provides basic collision detection while maintaining GPU performance + gpuData[i].colliderData = glm::vec4(5.0f, 5.0f, 5.0f, static_cast(2)); // 2 = Mesh (as Box) + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + default: + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); // Invalid + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + } } } - vulkanResources.physicsBufferMemory.unmapMemory(); - - // Reset counters + // Reset counters using persistent mapped memory uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count - data = vulkanResources.counterBufferMemory.mapMemory(0, sizeof(initialCounters)); - memcpy(data, initialCounters, sizeof(initialCounters)); - vulkanResources.counterBufferMemory.unmapMemory(); + memcpy(vulkanResources.persistentCounterMemory, initialCounters, sizeof(initialCounters)); // Update params buffer PhysicsParams params{}; - params.deltaTime = 1.0f / 60.0f; // Fixed time step - params.gravity = gravity; + params.deltaTime = deltaTime; // Use actual deltaTime instead of fixed timestep params.numBodies = static_cast(rigidBodies.size()); params.maxCollisions = maxGPUCollisions; + params.padding = 0.0f; // Initialize padding to zero for proper std140 alignment + params.gravity = glm::vec4(gravity, 0.0f); // Pack gravity into vec4 with padding - data = vulkanResources.paramsBufferMemory.mapMemory(0, sizeof(PhysicsParams)); - memcpy(data, ¶ms, sizeof(PhysicsParams)); - vulkanResources.paramsBufferMemory.unmapMemory(); + // Update params buffer using persistent mapped memory + memcpy(vulkanResources.persistentParamsMemory, ¶ms, sizeof(PhysicsParams)); + + // CRITICAL FIX: Explicit memory flush to ensure HOST_COHERENT memory is fully visible to GPU + // Even with HOST_COHERENT flag, some systems may have cache coherency issues with partial writes + // This ensures the entire PhysicsParams struct is flushed before GPU operations begin + try { + const vk::raii::Device& device = renderer->GetRaiiDevice(); + vk::MappedMemoryRange flushRange; + flushRange.memory = *vulkanResources.paramsBufferMemory; + flushRange.offset = 0; + flushRange.size = sizeof(PhysicsParams); + + device.flushMappedMemoryRanges(flushRange); + } catch (const std::exception& e) { + fprintf(stderr, "WARNING: Failed to flush params buffer memory: %s", e.what()); + } } void PhysicsSystem::ReadbackGPUPhysicsData() const { @@ -980,37 +1081,46 @@ void PhysicsSystem::ReadbackGPUPhysicsData() const { return; } - // Validate Vulkan resources before using them - if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE) { - std::cerr << "PhysicsSystem::ReadbackGPUPhysicsData: Invalid Vulkan resources" << std::endl; + // Validate Vulkan resources and persistent memory pointers before using them + if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || + !vulkanResources.persistentPhysicsMemory) { return; } - // Map the physics buffer - void* data = vulkanResources.physicsBufferMemory.mapMemory(0, sizeof(GPUPhysicsData) * rigidBodies.size()); + // Wait for a dedicated compute fence to ensure GPU compute operations are complete before reading back data + const vk::raii::Device& device = renderer->GetRaiiDevice(); + vk::Result result = device.waitForFences(*vulkanResources.computeFence, VK_TRUE, UINT64_MAX); + if (result != vk::Result::eSuccess) { + return; + } - // Copy physics data from the buffer - const auto* gpuData = static_cast(data); - for (size_t i = 0; i < rigidBodies.size(); i++) { - const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); + // Skip physics buffer operations if no rigid bodies exist + if (!rigidBodies.empty()) { + // Use persistent mapped memory for physics buffer readback + const auto* gpuData = static_cast(vulkanResources.persistentPhysicsMemory); + for (size_t i = 0; i < rigidBodies.size(); i++) { + const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); - // Skip kinematic bodies - if (concreteRigidBody->IsKinematic()) { - continue; - } + // Skip kinematic bodies + if (concreteRigidBody->IsKinematic()) { + continue; + } - concreteRigidBody->SetPosition(glm::vec3(gpuData[i].position)); - concreteRigidBody->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, - gpuData[i].rotation.y, gpuData[i].rotation.z)); - concreteRigidBody->SetLinearVelocity(glm::vec3(gpuData[i].linearVelocity)); - concreteRigidBody->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); - } + auto newPosition = glm::vec3(gpuData[i].position); + auto newVelocity = glm::vec3(gpuData[i].linearVelocity); - vulkanResources.physicsBufferMemory.unmapMemory(); + concreteRigidBody->SetPosition(newPosition); + concreteRigidBody->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, + gpuData[i].rotation.y, gpuData[i].rotation.z)); + concreteRigidBody->SetLinearVelocity(newVelocity); + concreteRigidBody->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); + } + } } -void PhysicsSystem::SimulatePhysicsOnGPU(float) const { +void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { if (!renderer) { + fprintf(stderr, "SimulatePhysicsOnGPU: No renderer available"); return; } @@ -1019,12 +1129,11 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float) const { *vulkanResources.integratePipeline == VK_NULL_HANDLE || *vulkanResources.pipelineLayout == VK_NULL_HANDLE || vulkanResources.descriptorSets.empty() || *vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBuffer == VK_NULL_HANDLE) { - std::cerr << "PhysicsSystem::SimulatePhysicsOnGPU: Invalid Vulkan resources" << std::endl; return; } // Update physics data on the GPU - UpdateGPUPhysicsData(); + UpdateGPUPhysicsData(deltaTime); // Reset the command buffer before beginning (required for reuse) vulkanResources.commandBuffer.reset(); @@ -1035,12 +1144,26 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float) const { vulkanResources.commandBuffer.begin(beginInfo); - // Bind descriptor set vulkanResources.commandBuffer.bindDescriptorSets( vk::PipelineBindPoint::eCompute, *vulkanResources.pipelineLayout, 0, - vulkanResources.descriptorSets, + **vulkanResources.descriptorSets.data(), + nullptr + ); + + // Add a memory barrier to ensure host-written uniform buffer data is visible to shader + // This ensures the PhysicsParams data uploaded from CPU is properly synchronized before shader execution + vk::MemoryBarrier hostToDeviceBarrier; + hostToDeviceBarrier.srcAccessMask = vk::AccessFlagBits::eHostWrite; + hostToDeviceBarrier.dstAccessMask = vk::AccessFlagBits::eUniformRead; + + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eHost, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + hostToDeviceBarrier, + nullptr, nullptr ); @@ -1064,9 +1187,9 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float) const { // Step 2: Broad-phase collision detection vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.broadPhasePipeline); - // Each thread checks one pair of objects uint32_t numPairs = (rigidBodies.size() * (rigidBodies.size() - 1)) / 2; - vulkanResources.commandBuffer.dispatch((numPairs + 63) / 64, 1, 1); + uint32_t broadPhaseThreads = (numPairs + 63) / 64; + vulkanResources.commandBuffer.dispatch(broadPhaseThreads, 1, 1); // Memory barrier to ensure the broad phase is complete before the narrow phase vulkanResources.commandBuffer.pipelineBarrier( @@ -1080,10 +1203,12 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float) const { // Step 3: Narrow-phase collision detection vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.narrowPhasePipeline); - // We don't know how many pairs were generated, so we use a conservative estimate - vulkanResources.commandBuffer.dispatch((maxGPUCollisions + 63) / 64, 1, 1); + // Dispatch enough threads to process all potential collision pairs found by broad-phase + // The shader will check counterBuffer[0] to determine the actual number of pairs to process + uint32_t narrowPhaseThreads = (maxGPUCollisions + 63) / 64; + vulkanResources.commandBuffer.dispatch(narrowPhaseThreads, 1, 1); - // Memory barrier to ensure a narrow phase is complete before resolution + // Memory barrier to ensure the narrow phase is complete before resolution vulkanResources.commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eComputeShader, @@ -1095,22 +1220,33 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float) const { // Step 4: Collision resolution vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.resolvePipeline); - // We don't know how many collisions were detected, so we use a conservative estimate - vulkanResources.commandBuffer.dispatch((maxGPUCollisions + 63) / 64, 1, 1); + uint32_t resolveThreads = (maxGPUCollisions + 63) / 64; + vulkanResources.commandBuffer.dispatch(resolveThreads, 1, 1); // End command buffer vulkanResources.commandBuffer.end(); - // Submit command buffer - vk::SubmitInfo submitInfo; - submitInfo.commandBufferCount = 1; - vk::CommandBuffer cmdBuffer = *vulkanResources.commandBuffer; - submitInfo.pCommandBuffers = &cmdBuffer; + // Reset fence before submitting new work + const vk::raii::Device& device = renderer->GetRaiiDevice(); + device.resetFences(*vulkanResources.computeFence); - vk::Queue computeQueue = renderer->GetComputeQueue(); - computeQueue.submit(submitInfo, nullptr); - computeQueue.waitIdle(); + // Submit command buffer with dedicated fence for synchronization + vk::CommandBuffer cmdBuffer = *vulkanResources.commandBuffer; + renderer->SubmitToComputeQueue(cmdBuffer, *vulkanResources.computeFence); - // Read back physics data from the GPU + // Read back physics data from the GPU (fence wait moved to ReadbackGPUPhysicsData) ReadbackGPUPhysicsData(); } + +void PhysicsSystem::CleanupMarkedBodies() { + // Remove rigid bodies that are marked for removal + auto it = rigidBodies.begin(); + while (it != rigidBodies.end()) { + auto concreteRigidBody = dynamic_cast(it->get()); + if (concreteRigidBody && concreteRigidBody->markedForRemoval) { + it = rigidBodies.erase(it); + } else { + ++it; + } + } +} \ No newline at end of file diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index 9a8b7ec2..b79d511a 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -99,25 +99,25 @@ class RigidBody { * @brief Get the position of the rigid body. * @return The position. */ - virtual glm::vec3 GetPosition() const = 0; + [[nodiscard]] virtual glm::vec3 GetPosition() const = 0; /** * @brief Get the rotation of the rigid body. * @return The rotation quaternion. */ - virtual glm::quat GetRotation() const = 0; + [[nodiscard]] virtual glm::quat GetRotation() const = 0; /** * @brief Get the linear velocity of the rigid body. * @return The linear velocity. */ - virtual glm::vec3 GetLinearVelocity() const = 0; + [[nodiscard]] virtual glm::vec3 GetLinearVelocity() const = 0; /** * @brief Get the angular velocity of the rigid body. * @return The angular velocity. */ - virtual glm::vec3 GetAngularVelocity() const = 0; + [[nodiscard]] virtual glm::vec3 GetAngularVelocity() const = 0; /** * @brief Set whether the rigid body is kinematic. @@ -129,7 +129,7 @@ class RigidBody { * @brief Check if the rigid body is kinematic. * @return True if kinematic, false otherwise. */ - virtual bool IsKinematic() const = 0; + [[nodiscard]] virtual bool IsKinematic() const = 0; }; /** @@ -160,10 +160,24 @@ struct GPUCollisionData { * @brief Structure for physics simulation parameters. */ struct PhysicsParams { - float deltaTime; // Time step - glm::vec3 gravity; // Gravity vector - uint32_t numBodies; // Number of rigid bodies - uint32_t maxCollisions; // Maximum number of collisions + float deltaTime; // Time step - 4 bytes + uint32_t numBodies; // Number of rigid bodies - 4 bytes + uint32_t maxCollisions; // Maximum number of collisions - 4 bytes + float padding; // Explicit padding to align gravity to 16-byte boundary - 4 bytes + glm::vec4 gravity; // Gravity vector (xyz) + padding (w) - 16 bytes + // Total: 32 bytes (aligned to 16-byte boundaries for std140 layout) +}; + +/** + * @brief Structure to store collision prediction data for ray-based collision system. + */ +struct CollisionPrediction { + float collisionTime = -1.0f; // Time within deltaTime when collision occurs (-1 = no collision) + glm::vec3 collisionPoint; // World position where collision occurs + glm::vec3 collisionNormal; // Surface normal at collision point + glm::vec3 newVelocity; // Predicted velocity after bounce + Entity* hitEntity = nullptr; // Entity that was hit + bool isValid = false; // Whether this prediction is valid }; /** @@ -223,7 +237,7 @@ class PhysicsSystem { * @brief Get the gravity of the physics world. * @return The gravity vector. */ - glm::vec3 GetGravity() const; + [[nodiscard]] glm::vec3 GetGravity() const; /** * @brief Perform a raycast. @@ -248,7 +262,7 @@ class PhysicsSystem { * @brief Check if GPU acceleration is enabled. * @return True if GPU acceleration is enabled, false otherwise. */ - bool IsGPUAccelerationEnabled() const { return gpuAccelerationEnabled; } + [[nodiscard]] bool IsGPUAccelerationEnabled() const { return gpuAccelerationEnabled; } /** * @brief Set the maximum number of objects that can be simulated on the GPU. @@ -262,7 +276,17 @@ class PhysicsSystem { */ void SetRenderer(Renderer* renderer) { this->renderer = renderer; } + /** + * @brief Set the current camera position for geometry-relative ball checking. + * @param cameraPosition The current camera position. + */ + void SetCameraPosition(const glm::vec3& cameraPosition) { this->cameraPosition = cameraPosition; } + private: + /** + * @brief Clean up rigid bodies that are marked for removal. + */ + void CleanupMarkedBodies(); // Rigid bodies std::vector> rigidBodies; @@ -278,6 +302,9 @@ class PhysicsSystem { uint32_t maxGPUCollisions = 4096; Renderer* renderer = nullptr; + // Camera position for geometry-relative ball checking + glm::vec3 cameraPosition = glm::vec3(0.0f, 0.0f, 0.0f); + // Vulkan resources for physics simulation struct VulkanResources { // Shader modules @@ -296,7 +323,7 @@ class PhysicsSystem { // Descriptor pool and sets vk::raii::DescriptorPool descriptorPool = nullptr; - std::vector descriptorSets; + std::vector descriptorSets; // Buffers for physics data vk::raii::Buffer physicsBuffer = nullptr; @@ -310,9 +337,17 @@ class PhysicsSystem { vk::raii::Buffer paramsBuffer = nullptr; vk::raii::DeviceMemory paramsBufferMemory = nullptr; + // Persistent mapped memory pointers for improved performance + void* persistentPhysicsMemory = nullptr; + void* persistentCounterMemory = nullptr; + void* persistentParamsMemory = nullptr; + // Command buffer for compute operations vk::raii::CommandPool commandPool = nullptr; vk::raii::CommandBuffer commandBuffer = nullptr; + + // Dedicated fence for compute synchronization + vk::raii::Fence computeFence = nullptr; }; VulkanResources vulkanResources; @@ -322,11 +357,11 @@ class PhysicsSystem { void CleanupVulkanResources(); // Update physics data on the GPU - void UpdateGPUPhysicsData() const; + void UpdateGPUPhysicsData(float deltaTime) const; // Read back physics data from the GPU void ReadbackGPUPhysicsData() const; // Perform GPU-accelerated physics simulation - void SimulatePhysicsOnGPU(float deltaTime) const; + void SimulatePhysicsOnGPU(float deltaTime); }; diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp index 37873eed..cea1cd9d 100644 --- a/attachments/simple_engine/platform.cpp +++ b/attachments/simple_engine/platform.cpp @@ -138,6 +138,11 @@ void AndroidPlatform::SetCharCallback(std::function callback) { charCallback = std::move(callback); } +void AndroidPlatform::SetWindowTitle(const std::string& title) { + // No-op on Android - mobile apps don't have window titles + (void)title; // Suppress unused parameter warning +} + void AndroidPlatform::DetectDeviceCapabilities() { if (!app) { return; @@ -423,6 +428,12 @@ void DesktopPlatform::SetCharCallback(std::function callback) { charCallback = std::move(callback); } +void DesktopPlatform::SetWindowTitle(const std::string& title) { + if (window) { + glfwSetWindowTitle(window, title.c_str()); + } +} + void DesktopPlatform::WindowResizeCallback(GLFWwindow* window, int width, int height) { auto* platform = static_cast(glfwGetWindowUserPointer(window)); platform->width = width; diff --git a/attachments/simple_engine/platform.h b/attachments/simple_engine/platform.h index d3e6d746..021aae31 100644 --- a/attachments/simple_engine/platform.h +++ b/attachments/simple_engine/platform.h @@ -118,6 +118,12 @@ class Platform { * @param callback The callback function to be called when character input is received. */ virtual void SetCharCallback(std::function callback) = 0; + + /** + * @brief Set the window title. + * @param title The new window title. + */ + virtual void SetWindowTitle(const std::string& title) = 0; }; #if PLATFORM_ANDROID @@ -273,6 +279,12 @@ class AndroidPlatform : public Platform { */ void SetCharCallback(std::function callback) override; + /** + * @brief Set the window title (no-op on Android). + * @param title The new window title. + */ + void SetWindowTitle(const std::string& title) override; + /** * @brief Get the Android app. * @return The Android app. @@ -418,6 +430,12 @@ class DesktopPlatform : public Platform { */ void SetCharCallback(std::function callback) override; + /** + * @brief Set the window title. + * @param title The new window title. + */ + void SetWindowTitle(const std::string& title) override; + /** * @brief Get the GLFW window. * @return The GLFW window. diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 3039e6ce..b979db45 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -401,6 +401,7 @@ class Renderer { static const std::string SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; static const std::string SHARED_DEFAULT_OCCLUSION_ID; static const std::string SHARED_DEFAULT_EMISSIVE_ID; + static const std::string SHARED_BRIGHT_RED_ID; /** * @brief Determine appropriate texture format based on texture type. diff --git a/attachments/simple_engine/renderer_compute.cpp b/attachments/simple_engine/renderer_compute.cpp index f00c75e6..65256065 100644 --- a/attachments/simple_engine/renderer_compute.cpp +++ b/attachments/simple_engine/renderer_compute.cpp @@ -258,6 +258,6 @@ vk::raii::Fence Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCo std::cerr << "Failed to dispatch compute shader: " << e.what() << std::endl; // Return a null fence on error vk::FenceCreateInfo fenceInfo{}; - return vk::raii::Fence(device, fenceInfo); + return {device, fenceInfo}; } } diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 52d6aca0..9ed2b274 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -17,19 +17,12 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { - // Check if this is a shader debug printf message - if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) { - std::string message(pCallbackData->pMessage); - if (message.find("DEBUG-PRINTF") != std::string::npos) { - // This is a shader debug printf message - always show it - std::cout << "FINDME ===== SHADER DEBUG: " << pCallbackData->pMessage << std::endl; - return VK_FALSE; - } - } - if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { // Print a message to the console std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; + } else { + // Print a message to the console + std::cout << "Validation layer: " << pCallbackData->pMessage << std::endl; } return VK_FALSE; diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index f2aaa2b1..cc0ae561 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -481,6 +481,14 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Render each entity for (auto entity : entities) { + // Check if ball-only rendering is enabled and filter entities accordingly + if (imguiSystem && imguiSystem->IsBallOnlyRenderingEnabled()) { + // Only render entities whose names contain "Ball_" + if (entity->GetName().find("Ball_") == std::string::npos) { + continue; // Skip non-ball entities + } + } + // Get the mesh component auto meshComponent = entity->GetComponent(); if (!meshComponent) { diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 281bd492..28b51051 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -23,6 +23,7 @@ const std::string Renderer::SHARED_DEFAULT_NORMAL_ID = "__shared_default_normal_ const std::string Renderer::SHARED_DEFAULT_METALLIC_ROUGHNESS_ID = "__shared_default_metallic_roughness__"; const std::string Renderer::SHARED_DEFAULT_OCCLUSION_ID = "__shared_default_occlusion__"; const std::string Renderer::SHARED_DEFAULT_EMISSIVE_ID = "__shared_default_emissive__"; +const std::string Renderer::SHARED_BRIGHT_RED_ID = "__shared_bright_red__"; // Create depth resources bool Renderer::createDepthResources() { @@ -327,6 +328,13 @@ bool Renderer::createSharedDefaultPBRTextures() { return false; } + // Create shared bright red texture for ball visibility + unsigned char brightRedPixel[4] = {255, 0, 0, 255}; // Bright red (R=255, G=0, B=0, A=255) + if (!LoadTextureFromMemory(SHARED_BRIGHT_RED_ID, brightRedPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared bright red texture" << std::endl; + return false; + } + std::cout << "Successfully created all shared default PBR textures" << std::endl; return true; } catch (const std::exception& e) { diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 3d769648..877591f6 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -216,19 +216,10 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, } int entitiesCreated = 0; - int smallObjectsCreated = 0; for (const auto& materialMesh : materialMeshes) { - // Calculate bounding box size to determine if this is a small object - glm::vec3 boundingBoxSize = CalculateBoundingBoxSize(materialMesh); - bool isSmall = IsSmallObject(boundingBoxSize); - - // Create an entity name based on model and material, with special marking for small objects + // Create an entity name based on model and material std::string entityName = modelName + "_Material_" + std::to_string(materialMesh.materialIndex) + "_" + materialMesh.materialName; - if (isSmall) { - entityName += "_SMALL_POKEABLE"; - smallObjectsCreated++; - } if (Entity* materialEntity = engine->CreateEntity(entityName)) { // Add a transform component with provided parameters @@ -271,6 +262,21 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Continue with other entities even if one fails } + // Create physics body for collision with balls + // Use mesh collision shape for accurate geometry interaction + PhysicsSystem* physicsSystem = engine->GetPhysicsSystem(); + if (physicsSystem) { + RigidBody* rigidBody = physicsSystem->CreateRigidBody(materialEntity, CollisionShape::Mesh, 0.0f); // Mass 0 = static + if (rigidBody) { + rigidBody->SetKinematic(true); // Static geometry doesn't move + rigidBody->SetRestitution(0.15f); // Very low bounce - balls lose 85%+ momentum + rigidBody->SetFriction(0.5f); // Moderate friction + std::cout << "Created physics body for geometry entity: " << entityName << std::endl; + } else { + std::cerr << "Failed to create physics body for entity: " << entityName << std::endl; + } + } + entitiesCreated++; } else { std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; @@ -278,7 +284,6 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, } std::cout << "Successfully created " << entitiesCreated << " entities from loaded materials" << std::endl; - std::cout << " - " << smallObjectsCreated << " small pokeable objects identified" << std::endl; } catch (const std::exception& e) { std::cerr << "Error loading GLTF model: " << e.what() << std::endl; diff --git a/attachments/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang index 4772c25d..be0b626a 100644 --- a/attachments/simple_engine/shaders/physics.slang +++ b/attachments/simple_engine/shaders/physics.slang @@ -32,10 +32,12 @@ struct CollisionData { [[vk::binding(4, 0)]] ConstantBuffer params; struct PhysicsParams { - float deltaTime; // Time step - float3 gravity; // Gravity vector - uint numBodies; // Number of rigid bodies - uint maxCollisions; // Maximum number of collisions + float deltaTime; // Time step - 4 bytes + uint numBodies; // Number of rigid bodies - 4 bytes + uint maxCollisions; // Maximum number of collisions - 4 bytes + float padding; // Explicit padding to align gravity to 16-byte boundary - 4 bytes + float4 gravity; // Gravity vector (xyz) + padding (w) - 16 bytes + // Total: 32 bytes (aligned to 16-byte boundaries for std140 layout) }; // Quaternion multiplication @@ -71,6 +73,7 @@ void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { // Get physics data for this body PhysicsData body = physicsBuffer[index]; + // Skip kinematic bodies if (body.force.w > 0.5) { return; @@ -78,13 +81,17 @@ void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { // Apply gravity if enabled if (body.torque.w > 0.5) { - body.force.xyz += params.gravity / body.position.w; + float3 gravityForce = params.gravity.xyz * body.position.w; + body.force.xyz += gravityForce; + } // Integrate forces - body.linearVelocity.xyz += body.force.xyz * body.position.w * params.deltaTime; + float3 velocityChange = body.force.xyz * body.position.w * params.deltaTime; + body.linearVelocity.xyz += velocityChange; body.angularVelocity.xyz += body.torque.xyz * params.deltaTime; // Simplified, should use inertia tensor + // Apply damping const float linearDamping = 0.01; const float angularDamping = 0.01; @@ -92,7 +99,9 @@ void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { body.angularVelocity.xyz *= (1.0 - angularDamping); // Integrate velocities - body.position.xyz += body.linearVelocity.xyz * params.deltaTime; + float3 positionChange = body.linearVelocity.xyz * params.deltaTime; + body.position.xyz += positionChange; + // Update rotation float4 angularVelocityQuat = float4(body.angularVelocity.xyz * 0.5, 0.0); @@ -101,6 +110,7 @@ void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { // Write updated data back to buffer physicsBuffer[index] = body; + } // Compute AABB for a body @@ -125,6 +135,13 @@ void computeAABB(PhysicsData body, out float3 min, out float3 max) { min = center - halfExtents; max = center + halfExtents; } + else if (colliderType == 2) { // Mesh (represented as large bounding box) + float3 halfExtents = body.colliderData.xyz; + float3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } } // Check if two AABBs overlap @@ -145,21 +162,28 @@ void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { } // Convert linear index to pair indices (i, j) where i < j + // Use a more robust algorithm that avoids floating-point precision issues uint i = 0; uint j = 0; - // This is a mathematical formula to convert a linear index to a pair of indices - uint row = uint(floor(sqrt(float(2 * index + 0.25)) - 0.5)); - i = row; - j = index - (row * (row + 1)) / 2; + // Find i and j using integer arithmetic to avoid precision errors + uint remaining = index; + uint currentRow = 0; - // Ensure j > i - j += i + 1; + // Find which row (i value) this index belongs to + while (remaining >= (params.numBodies - 1 - currentRow)) { + remaining -= (params.numBodies - 1 - currentRow); + currentRow++; + } + + i = currentRow; + j = i + 1 + remaining; // Get physics data for both bodies PhysicsData bodyA = physicsBuffer[i]; PhysicsData bodyB = physicsBuffer[j]; + // Skip if both bodies are kinematic if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { return; @@ -211,7 +235,7 @@ void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { int shapeA = int(bodyA.colliderData.w); int shapeB = int(bodyB.colliderData.w); - // Only handle sphere-sphere collisions for simplicity + // Handle sphere-sphere collisions if (shapeA == 0 && shapeB == 0) { // Both are spheres float radiusA = bodyA.colliderData.x; float radiusB = bodyB.colliderData.x; @@ -244,7 +268,46 @@ void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { } } } - // Add other collision types here (box-box, sphere-box, etc.) + // Handle sphere-geometry collisions (sphere vs mesh represented as box) + else if ((shapeA == 0 && shapeB == 2) || (shapeA == 2 && shapeB == 0)) { + // Determine which is sphere and which is geometry + PhysicsData sphere = (shapeA == 0) ? bodyA : bodyB; + PhysicsData geometry = (shapeA == 0) ? bodyB : bodyA; + uint sphereIndex = (shapeA == 0) ? bodyIndexA : bodyIndexB; + uint geometryIndex = (shapeA == 0) ? bodyIndexB : bodyIndexA; + + float sphereRadius = sphere.colliderData.x; + float3 spherePos = sphere.position.xyz + sphere.colliderData2.xyz; + float3 geometryPos = geometry.position.xyz + geometry.colliderData2.xyz; + float3 geometryHalfExtents = geometry.colliderData.xyz; + + // Simple sphere-box collision detection + float3 closestPoint = clamp(spherePos, geometryPos - geometryHalfExtents, geometryPos + geometryHalfExtents); + float3 direction = spherePos - closestPoint; + float distance = length(direction); + + if (distance < sphereRadius) { + // Collision detected + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + + if (collisionIndex < params.maxCollisions) { + // Calculate normal (from geometry to sphere) + float3 normal = (distance > 0.0001) ? direction / distance : float3(0, 1, 0); + float penetration = sphereRadius - distance; + + // Create collision data + CollisionData collision; + collision.bodyA = sphereIndex; + collision.bodyB = geometryIndex; + collision.contactNormal = float4(normal, penetration); + collision.contactPoint = float4(closestPoint, 0); + + // Store collision data + collisionBuffer[collisionIndex] = collision; + } + } + } } // Collision resolution - resolves detected collisions From bdc184549005c43f684da7289ec8d794cc88e70d Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 31 Jul 2025 11:53:12 -0700 Subject: [PATCH 041/102] add in the dependences install scripts. --- attachments/simple_engine/audio_system.cpp | 9 - attachments/simple_engine/engine.cpp | 20 -- .../install_dependencies_linux.sh | 154 ++++++++++++++ .../install_dependencies_windows.bat | 191 ++++++++++++++++++ 4 files changed, 345 insertions(+), 29 deletions(-) create mode 100755 attachments/simple_engine/install_dependencies_linux.sh create mode 100644 attachments/simple_engine/install_dependencies_windows.bat diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 936ab00a..4af3b490 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -183,9 +183,6 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { this->channels = channels; this->bufferSize = bufferSize; - std::cout << "Initializing OpenAL audio output device: " << sampleRate << "Hz, " - << channels << " channels, buffer size: " << bufferSize << std::endl; - // Initialize OpenAL device = alcOpenDevice(nullptr); // Use default device if (!device) { @@ -359,8 +356,6 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { } void AudioThreadFunction() { - std::cout << "OpenAL audio playback thread started" << std::endl; - // Calculate sleep time for audio buffer updates (in milliseconds) const auto sleepTime = std::chrono::milliseconds( static_cast((bufferSize * 1000) / sampleRate / 8) // Eighth buffer time for responsiveness @@ -370,8 +365,6 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { ProcessAudioBuffer(); std::this_thread::sleep_for(sleepTime); } - - std::cout << "OpenAL audio playback thread stopped" << std::endl; } void ProcessAudioBuffer() { @@ -1364,7 +1357,6 @@ void AudioSystem::startAudioThread() { audioThreadRunning.store(true); audioThread = std::thread(&AudioSystem::audioThreadLoop, this); - std::cout << "Audio processing thread started" << std::endl; } void AudioSystem::stopAudioThread() { @@ -1384,7 +1376,6 @@ void AudioSystem::stopAudioThread() { } audioThreadRunning.store(false); - std::cout << "Audio processing thread stopped" << std::endl; } void AudioSystem::audioThreadLoop() { diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 74d9b858..f8ebc786 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -564,11 +564,6 @@ void Engine::GenerateBallMaterial() { // Very low bounciness (0.05 to 0.15 for 85-95% momentum loss per bounce) ballMaterial.bounciness = 0.05f + dis(gen) * 0.1f; - - std::cout << "Generated ball material - Albedo: (" << ballMaterial.albedo.x << ", " - << ballMaterial.albedo.y << ", " << ballMaterial.albedo.z << "), " - << "Metallic: " << ballMaterial.metallic << ", Roughness: " << ballMaterial.roughness - << ", Bounciness: " << ballMaterial.bounciness << std::endl; } void Engine::InitializePhysicsScaling() { @@ -585,12 +580,6 @@ void Engine::InitializePhysicsScaling() { physicsScaling.physicsTimeScale = 1.0f; // Keep time scale normal physicsScaling.gravityScale = 1.0f; // Keep gravity proportional to scale - std::cout << "Physics scaling initialized:" << std::endl; - std::cout << " Game units to meters: " << physicsScaling.gameUnitsToMeters << std::endl; - std::cout << " Force scale: " << physicsScaling.forceScale << std::endl; - std::cout << " Time scale: " << physicsScaling.physicsTimeScale << std::endl; - std::cout << " Gravity scale: " << physicsScaling.gravityScale << std::endl; - // Apply scaled gravity to physics system glm::vec3 realWorldGravity(0.0f, -9.81f, 0.0f); glm::vec3 scaledGravity = ScaleGravityForPhysics(realWorldGravity); @@ -679,12 +668,6 @@ void Engine::ThrowBall(float mouseX, float mouseY) { spawnPosition.y += posDis(gen); spawnPosition.z += posDis(gen); - // Log camera, screen center, and spawn positions for debugging - std::cout << "CAMERA POSITION: (" << cameraPosition.x << ", " << cameraPosition.y << ", " << cameraPosition.z << ")" << std::endl; - std::cout << "SCREEN CENTER WORLD POS: (" << screenCenterWorldPos.x << ", " << screenCenterWorldPos.y << ", " << screenCenterWorldPos.z << ")" << std::endl; - std::cout << "BALL SPAWN POSITION: (" << spawnPosition.x << ", " << spawnPosition.y << ", " << spawnPosition.z << ")" << std::endl; - std::cout << "THROW DIRECTION: (" << throwDirection.x << ", " << throwDirection.y << ", " << throwDirection.z << ")" << std::endl; - std::cout << "MOUSE NDC: (" << ndcX << ", " << ndcY << ")" << std::endl; std::uniform_real_distribution spinDis(-10.0f, 10.0f); std::uniform_real_distribution forceDis(15.0f, 35.0f); // Stronger force range for proper throwing feel @@ -757,9 +740,6 @@ void Engine::ProcessPendingBalls() { glm::vec3 throwImpulse = pendingBall.throwDirection * pendingBall.throwForce; rigidBody->ApplyImpulse(throwImpulse, glm::vec3(0.0f)); rigidBody->SetAngularVelocity(pendingBall.randomSpin); - - std::cout << "Ball " << pendingBall.ballName << " created successfully with force " - << pendingBall.throwForce << std::endl; } } diff --git a/attachments/simple_engine/install_dependencies_linux.sh b/attachments/simple_engine/install_dependencies_linux.sh new file mode 100755 index 00000000..62cc7b55 --- /dev/null +++ b/attachments/simple_engine/install_dependencies_linux.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +# Install script for Simple Game Engine dependencies on Linux +# This script installs all required dependencies for building the Simple Game Engine + +set -e # Exit on any error + +echo "Installing Simple Game Engine dependencies for Linux..." + +# Detect the Linux distribution +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID +elif type lsb_release >/dev/null 2>&1; then + OS=$(lsb_release -si) + VER=$(lsb_release -sr) +elif [ -f /etc/lsb-release ]; then + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE +elif [ -f /etc/debian_version ]; then + OS=Debian + VER=$(cat /etc/debian_version) +else + OS=$(uname -s) + VER=$(uname -r) +fi + +echo "Detected OS: $OS $VER" + +# Function to install dependencies on Ubuntu/Debian +install_ubuntu_debian() { + echo "Installing dependencies for Ubuntu/Debian..." + + # Update package list + sudo apt update + + # Install build essentials + sudo apt install -y build-essential cmake git + + # Install Vulkan SDK + echo "Installing Vulkan SDK..." + wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - + sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list https://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list + sudo apt update + sudo apt install -y vulkan-sdk + + # Install other dependencies + sudo apt install -y \ + libglfw3-dev \ + libglm-dev \ + libopenal-dev \ + libktx-dev \ + libstb-dev + + # Install Slang compiler (for shader compilation) + echo "Installing Slang compiler..." + if [ ! -f /usr/local/bin/slangc ]; then + SLANG_VERSION="2024.1.21" + wget "https://github.com/shader-slang/slang/releases/download/v${SLANG_VERSION}/slang-${SLANG_VERSION}-linux-x86_64.tar.gz" + tar -xzf "slang-${SLANG_VERSION}-linux-x86_64.tar.gz" + sudo cp slang/bin/slangc /usr/local/bin/ + sudo chmod +x /usr/local/bin/slangc + rm -rf slang "slang-${SLANG_VERSION}-linux-x86_64.tar.gz" + fi +} + +# Function to install dependencies on Fedora/RHEL/CentOS +install_fedora_rhel() { + echo "Installing dependencies for Fedora/RHEL/CentOS..." + + # Install build essentials + sudo dnf install -y gcc gcc-c++ cmake git + + # Install Vulkan SDK + echo "Installing Vulkan SDK..." + sudo dnf install -y vulkan-devel vulkan-tools + + # Install other dependencies + sudo dnf install -y \ + glfw-devel \ + glm-devel \ + openal-soft-devel + + # Note: Some packages might need to be built from source on RHEL/CentOS + echo "Note: Some dependencies (libktx, libstb, tinygltf) may need to be built from source" + echo "Please refer to the project documentation for manual installation instructions" +} + +# Function to install dependencies on Arch Linux +install_arch() { + echo "Installing dependencies for Arch Linux..." + + # Update package database + sudo pacman -Sy + + # Install build essentials + sudo pacman -S --noconfirm base-devel cmake git + + # Install dependencies + sudo pacman -S --noconfirm \ + vulkan-devel \ + glfw-wayland \ + glm \ + openal + + # Install AUR packages (requires yay or another AUR helper) + if command -v yay &> /dev/null; then + yay -S --noconfirm libktx stb + else + echo "Note: Please install yay or another AUR helper to install libktx and stb packages" + echo "Alternatively, build these dependencies from source" + fi +} + +# Install dependencies based on detected OS +case "$OS" in + "Ubuntu"* | "Debian"* | "Linux Mint"*) + install_ubuntu_debian + ;; + "Fedora"* | "Red Hat"* | "CentOS"* | "Rocky"*) + install_fedora_rhel + ;; + "Arch"* | "Manjaro"*) + install_arch + ;; + *) + echo "Unsupported Linux distribution: $OS" + echo "Please install the following dependencies manually:" + echo "- CMake (3.29 or later)" + echo "- Vulkan SDK" + echo "- GLFW3 development libraries" + echo "- GLM (OpenGL Mathematics) library" + echo "- OpenAL development libraries" + echo "- KTX library" + echo "- STB library" + echo "- tinygltf library" + echo "- Slang compiler" + exit 1 + ;; +esac + +echo "" +echo "Dependencies installation completed!" +echo "" +echo "To build the Simple Game Engine:" +echo "1. cd to the simple_engine directory" +echo "2. mkdir build && cd build" +echo "3. cmake .." +echo "4. make -j$(nproc)" +echo "" +echo "Or use the provided CMake build command:" +echo "cmake --build cmake-build-debug --target SimpleEngine -j 10" diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat new file mode 100644 index 00000000..c9ea0686 --- /dev/null +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -0,0 +1,191 @@ +@echo off +REM Install script for Simple Game Engine dependencies on Windows +REM This script installs all required dependencies for building the Simple Game Engine + +echo Installing Simple Game Engine dependencies for Windows... + +REM Check if running as administrator +net session >nul 2>&1 +if %errorLevel% == 0 ( + echo Running as administrator - good! +) else ( + echo This script requires administrator privileges. + echo Please run as administrator. + pause + exit /b 1 +) + +REM Check if vcpkg is installed +where vcpkg >nul 2>&1 +if %errorLevel% == 0 ( + echo vcpkg found in PATH +) else ( + echo vcpkg not found in PATH. Installing vcpkg... + + REM Install vcpkg + if not exist "C:\vcpkg" ( + echo Cloning vcpkg to C:\vcpkg... + git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg + if %errorLevel% neq 0 ( + echo Failed to clone vcpkg. Please install Git first. + pause + exit /b 1 + ) + ) + + REM Bootstrap vcpkg + echo Bootstrapping vcpkg... + cd /d C:\vcpkg + call bootstrap-vcpkg.bat + if %errorLevel% neq 0 ( + echo Failed to bootstrap vcpkg. + pause + exit /b 1 + ) + + REM Add vcpkg to PATH for this session + set PATH=%PATH%;C:\vcpkg + + REM Integrate vcpkg with Visual Studio + echo Integrating vcpkg with Visual Studio... + vcpkg integrate install +) + +REM Check if Chocolatey is installed for additional packages +where choco >nul 2>&1 +if %errorLevel% neq 0 ( + echo Installing Chocolatey package manager... + powershell -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" + if %errorLevel% neq 0 ( + echo Failed to install Chocolatey. Some dependencies may need manual installation. + ) +) + +REM Install CMake if not present +where cmake >nul 2>&1 +if %errorLevel% neq 0 ( + echo Installing CMake... + choco install cmake -y + if %errorLevel% neq 0 ( + echo Failed to install CMake via Chocolatey. Please install manually from https://cmake.org/download/ + ) +) + +REM Install Git if not present +where git >nul 2>&1 +if %errorLevel% neq 0 ( + echo Installing Git... + choco install git -y + if %errorLevel% neq 0 ( + echo Failed to install Git via Chocolatey. Please install manually from https://git-scm.com/download/win + ) +) + +REM Install Vulkan SDK +echo Installing Vulkan SDK... +if not exist "C:\VulkanSDK" ( + echo Downloading and installing Vulkan SDK... + choco install vulkan-sdk -y + if %errorLevel% neq 0 ( + echo Failed to install Vulkan SDK via Chocolatey. + echo Please download and install manually from https://vulkan.lunarg.com/sdk/home#windows + echo Make sure to set the VULKAN_SDK environment variable. + ) +) else ( + echo Vulkan SDK appears to be already installed. +) + +REM Install vcpkg packages +echo Installing dependencies via vcpkg... + +REM Set vcpkg triplet for x64 Windows +set VCPKG_DEFAULT_TRIPLET=x64-windows + +REM Install GLFW +echo Installing GLFW... +vcpkg install glfw3:x64-windows +if %errorLevel% neq 0 ( + echo Warning: Failed to install GLFW via vcpkg +) + +REM Install GLM +echo Installing GLM... +vcpkg install glm:x64-windows +if %errorLevel% neq 0 ( + echo Warning: Failed to install GLM via vcpkg +) + +REM Install OpenAL +echo Installing OpenAL... +vcpkg install openal-soft:x64-windows +if %errorLevel% neq 0 ( + echo Warning: Failed to install OpenAL via vcpkg +) + +REM Install KTX +echo Installing KTX... +vcpkg install ktx:x64-windows +if %errorLevel% neq 0 ( + echo Warning: Failed to install KTX via vcpkg +) + +REM Install STB +echo Installing STB... +vcpkg install stb:x64-windows +if %errorLevel% neq 0 ( + echo Warning: Failed to install STB via vcpkg +) + +REM Install tinygltf +echo Installing tinygltf... +vcpkg install tinygltf:x64-windows +if %errorLevel% neq 0 ( + echo Warning: Failed to install tinygltf via vcpkg +) + +REM Install Slang compiler +echo Installing Slang compiler... +if not exist "C:\Program Files\Slang" ( + echo Downloading Slang compiler... + set SLANG_VERSION=2024.1.21 + powershell -Command "Invoke-WebRequest -Uri 'https://github.com/shader-slang/slang/releases/download/v%SLANG_VERSION%/slang-%SLANG_VERSION%-win64.zip' -OutFile 'slang-win64.zip'" + if %errorLevel% == 0 ( + echo Extracting Slang compiler... + powershell -Command "Expand-Archive -Path 'slang-win64.zip' -DestinationPath 'C:\Program Files\Slang' -Force" + del slang-win64.zip + + REM Add Slang to PATH (requires restart or new command prompt) + echo Adding Slang to system PATH... + setx PATH "%PATH%;C:\Program Files\Slang\bin" /M + echo Note: You may need to restart your command prompt for Slang to be available in PATH + ) else ( + echo Failed to download Slang compiler. Please install manually from: + echo https://github.com/shader-slang/slang/releases + ) +) else ( + echo Slang compiler appears to be already installed. +) + +REM Set environment variables for CMake to find vcpkg +echo Setting up CMake integration... +setx CMAKE_TOOLCHAIN_FILE "C:\vcpkg\scripts\buildsystems\vcpkg.cmake" /M +setx VCPKG_TARGET_TRIPLET "x64-windows" /M + +echo. +echo Dependencies installation completed! +echo. +echo To build the Simple Game Engine: +echo 1. Open a new command prompt (to get updated PATH) +echo 2. cd to the simple_engine directory +echo 3. mkdir build ^&^& cd build +echo 4. cmake .. -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake +echo 5. cmake --build . --config Release +echo. +echo Or use Visual Studio: +echo 1. Open the CMakeLists.txt file in Visual Studio +echo 2. Visual Studio should automatically detect vcpkg integration +echo 3. Build the project using Ctrl+Shift+B +echo. +echo Note: You may need to restart your command prompt or IDE for environment variables to take effect. + +pause From cdb01dbed3631292ff01182ff96385eae06d072f Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 1 Aug 2025 11:18:36 -0700 Subject: [PATCH 042/102] attempt to fancy fix the windows install script. --- attachments/simple_engine/install_dependencies_windows.bat | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index c9ea0686..ffc1b2f5 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -156,7 +156,8 @@ if not exist "C:\Program Files\Slang" ( REM Add Slang to PATH (requires restart or new command prompt) echo Adding Slang to system PATH... - setx PATH "%PATH%;C:\Program Files\Slang\bin" /M + for /f "tokens=2*" %%A in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PATH') do set "CURRENT_PATH=%%B" + setx PATH "%CURRENT_PATH%;C:\Program Files\Slang\bin" /M echo Note: You may need to restart your command prompt for Slang to be available in PATH ) else ( echo Failed to download Slang compiler. Please install manually from: From 16019cad89e2efafec5273bb5787360c7d513c23 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 8 Aug 2025 10:34:06 -0700 Subject: [PATCH 043/102] CMake: use PLATFORM_* defines via ANDROID, fix glfw condition, make slangc optional per review --- attachments/simple_engine/CMakeLists.txt | 42 +++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 55ea1295..612a0214 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -66,24 +66,28 @@ endif() # Find Slang shaders file(GLOB SLANG_SHADER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.slang) -# Find slangc executable -find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin REQUIRED) - -# Compile Slang shaders using slangc -foreach(SHADER ${SLANG_SHADER_SOURCES}) - get_filename_component(SHADER_NAME ${SHADER} NAME) - get_filename_component(SHADER_NAME_WE ${SHADER_NAME} NAME_WE) - string(REGEX REPLACE "\.slang$" "" OUTPUT_NAME ${SHADER_NAME}) - add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv - COMMAND ${SLANGC_EXECUTABLE} ${SHADER} -target spirv -profile spirv_1_4 -emit-spirv-directly -o ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv - DEPENDS ${SHADER} - COMMENT "Compiling Slang shader ${SHADER_NAME} with slangc" - ) - list(APPEND SHADER_SPVS ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv) -endforeach() - -add_custom_target(shaders DEPENDS ${SHADER_SPVS}) +# Find slangc executable (optional) +find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin) + +# Compile Slang shaders using slangc if available +if(SLANGC_EXECUTABLE) + foreach(SHADER ${SLANG_SHADER_SOURCES}) + get_filename_component(SHADER_NAME ${SHADER} NAME) + get_filename_component(SHADER_NAME_WE ${SHADER_NAME} NAME_WE) + string(REGEX REPLACE "\.slang$" "" OUTPUT_NAME ${SHADER_NAME}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + COMMAND ${SLANGC_EXECUTABLE} ${SHADER} -target spirv -profile spirv_1_4 -emit-spirv-directly -o ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + DEPENDS ${SHADER} + COMMENT "Compiling Slang shader ${SHADER_NAME} with slangc" + ) + list(APPEND SHADER_SPVS ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv) + endforeach() + add_custom_target(shaders DEPENDS ${SHADER_SPVS}) +else() + message(STATUS "slangc not found. Skipping shader compilation step.") + add_custom_target(shaders) +endif() # Source files set(SOURCES @@ -131,7 +135,7 @@ target_link_libraries(SimpleEngine PRIVATE OpenAL::OpenAL ) -if(NOT DEFINED ANDROID) +if(NOT ANDROID) target_link_libraries(SimpleEngine PRIVATE glfw) endif() From 4b8198dc0c114c16ef290a126927e41a58440fb0 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 8 Aug 2025 15:12:19 -0700 Subject: [PATCH 044/102] Fix the pathing for install_dependencies_windows.bat also fix CMakeLists.txt slang pathing. --- attachments/simple_engine/CMakeLists.txt | 16 ++++++++++------ .../install_dependencies_windows.bat | 12 +++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 612a0214..b5d14874 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -54,12 +54,10 @@ target_sources(VulkanCppModule # Platform-specific settings if(ANDROID) # Android-specific settings - add_definitions(-DPLATFORM_ANDROID=1) - add_definitions(-DPLATFORM_DESKTOP=0) + add_definitions(-DPLATFORM_ANDROID) else() # Desktop-specific settings - add_definitions(-DPLATFORM_ANDROID=0) - add_definitions(-DPLATFORM_DESKTOP=1) + add_definitions(-DPLATFORM_DESKTOP) endif() # Shader compilation @@ -69,8 +67,11 @@ file(GLOB SLANG_SHADER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.slang) # Find slangc executable (optional) find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin) -# Compile Slang shaders using slangc if available if(SLANGC_EXECUTABLE) + # Ensure the output directory for compiled shaders exists + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders) + + # Compile Slang shaders using slangc foreach(SHADER ${SLANG_SHADER_SOURCES}) get_filename_component(SHADER_NAME ${SHADER} NAME) get_filename_component(SHADER_NAME_WE ${SHADER_NAME} NAME_WE) @@ -83,6 +84,7 @@ if(SLANGC_EXECUTABLE) ) list(APPEND SHADER_SPVS ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv) endforeach() + add_custom_target(shaders DEPENDS ${SHADER_SPVS}) else() message(STATUS "slangc not found. Skipping shader compilation step.") @@ -189,7 +191,9 @@ endif() # Include binary and resource directories in the package install(TARGETS SimpleEngine DESTINATION bin) -install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders DESTINATION share/SimpleEngine) +if(SLANGC_EXECUTABLE) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders DESTINATION share/SimpleEngine) +endif() # Install models and textures if they exist if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/models) diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index ffc1b2f5..e74ec088 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -61,11 +61,14 @@ if %errorLevel% neq 0 ( ) ) +set "CHOCO_EXE=choco" +if exist "%ProgramData%\chocolatey\bin\choco.exe" set "CHOCO_EXE=%ProgramData%\chocolatey\bin\choco.exe" + REM Install CMake if not present where cmake >nul 2>&1 if %errorLevel% neq 0 ( echo Installing CMake... - choco install cmake -y + "%CHOCO_EXE%" install cmake -y if %errorLevel% neq 0 ( echo Failed to install CMake via Chocolatey. Please install manually from https://cmake.org/download/ ) @@ -75,7 +78,7 @@ REM Install Git if not present where git >nul 2>&1 if %errorLevel% neq 0 ( echo Installing Git... - choco install git -y + "%CHOCO_EXE%" install git -y if %errorLevel% neq 0 ( echo Failed to install Git via Chocolatey. Please install manually from https://git-scm.com/download/win ) @@ -85,7 +88,7 @@ REM Install Vulkan SDK echo Installing Vulkan SDK... if not exist "C:\VulkanSDK" ( echo Downloading and installing Vulkan SDK... - choco install vulkan-sdk -y + "%CHOCO_EXE%" install vulkan-sdk -y if %errorLevel% neq 0 ( echo Failed to install Vulkan SDK via Chocolatey. echo Please download and install manually from https://vulkan.lunarg.com/sdk/home#windows @@ -156,8 +159,7 @@ if not exist "C:\Program Files\Slang" ( REM Add Slang to PATH (requires restart or new command prompt) echo Adding Slang to system PATH... - for /f "tokens=2*" %%A in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PATH') do set "CURRENT_PATH=%%B" - setx PATH "%CURRENT_PATH%;C:\Program Files\Slang\bin" /M + powershell -NoProfile -ExecutionPolicy Bypass -Command "$p=[Environment]::GetEnvironmentVariable('Path','Machine'); if(-not ($p -split ';' | ForEach-Object { $_.ToLower() }) -contains 'c:\\program files\\slang\\bin'){ [Environment]::SetEnvironmentVariable('Path',($p + ';C:\\Program Files\\Slang\\bin'),'Machine'); Write-Host 'Added Slang to machine PATH'; } else { Write-Host 'Slang already in machine PATH'; }" echo Note: You may need to restart your command prompt for Slang to be available in PATH ) else ( echo Failed to download Slang compiler. Please install manually from: From 14dcb357c38c2350a4cfdb42e937219820dd420b Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 10 Aug 2025 01:19:22 -0700 Subject: [PATCH 045/102] Add instancing support, remove unused STB dependency, and fetch Bistro assets - Introduced instance data structure for instanced rendering. - Updated Vulkan pipeline to include instancing with vertex and instance attribute descriptions. - Removed STB library from dependencies and scripts. - Included Linux/macOS and Windows scripts to fetch Bistro example assets. --- attachments/simple_engine/CMakeLists.txt | 2 - attachments/simple_engine/camera_component.h | 8 + attachments/simple_engine/engine.cpp | 34 +- attachments/simple_engine/engine.h | 2 +- .../simple_engine/fetch_bistro_assets.bat | 55 + .../simple_engine/fetch_bistro_assets.sh | 38 + .../install_dependencies_linux.sh | 9 +- .../install_dependencies_windows.bat | 7 - attachments/simple_engine/main.cpp | 2 +- attachments/simple_engine/memory_pool.cpp | 14 +- attachments/simple_engine/mesh_component.cpp | 98 +- attachments/simple_engine/mesh_component.h | 309 ++++- attachments/simple_engine/model_loader.cpp | 1154 +++++++++-------- attachments/simple_engine/model_loader.h | 96 +- attachments/simple_engine/physics_system.cpp | 132 +- attachments/simple_engine/physics_system.h | 22 +- attachments/simple_engine/platform.cpp | 2 +- attachments/simple_engine/platform.h | 6 +- attachments/simple_engine/renderer.h | 82 +- attachments/simple_engine/renderer_core.cpp | 133 +- .../simple_engine/renderer_pipelines.cpp | 102 +- .../simple_engine/renderer_rendering.cpp | 143 +- .../simple_engine/renderer_resources.cpp | 496 +++++-- attachments/simple_engine/renderer_utils.cpp | 1 + attachments/simple_engine/scene_loading.cpp | 111 +- attachments/simple_engine/shaders/pbr.slang | 38 +- .../simple_engine/shaders/physics.slang | 8 + .../simple_engine/shaders/texturedMesh.slang | 47 +- en/Building_a_Simple_Engine/introduction.adoc | 21 + 29 files changed, 1972 insertions(+), 1200 deletions(-) create mode 100644 attachments/simple_engine/fetch_bistro_assets.bat create mode 100755 attachments/simple_engine/fetch_bistro_assets.sh diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index b5d14874..a2a06819 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -14,7 +14,6 @@ find_package (glm REQUIRED) find_package (Vulkan REQUIRED) find_package (tinygltf REQUIRED) find_package (KTX REQUIRED) -find_package (stb REQUIRED) find_package (OpenAL REQUIRED) # set up Vulkan C++ module @@ -133,7 +132,6 @@ target_link_libraries(SimpleEngine PRIVATE glm::glm tinygltf::tinygltf KTX::ktx - stb::stb OpenAL::OpenAL ) diff --git a/attachments/simple_engine/camera_component.h b/attachments/simple_engine/camera_component.h index 82d0b25c..93968de1 100644 --- a/attachments/simple_engine/camera_component.h +++ b/attachments/simple_engine/camera_component.h @@ -198,6 +198,14 @@ class CameraComponent : public Component { return up; } + /** + * @brief Force view matrix recalculation without modifying camera orientation. + * This is used when the camera's transform position changes externally (e.g., from GLTF loading). + */ + void ForceViewMatrixUpdate() { + viewMatrixDirty = true; + } + private: /** * @brief Update the view matrix based on the camera position and target. diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index f8ebc786..af8cc736 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -25,7 +25,7 @@ Engine::~Engine() { bool Engine::Initialize(const std::string& appName, int width, int height, bool enableValidationLayers) { // Create platform -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) // For Android, the platform is created with the android_app // This will be handled in the android_main function return false; @@ -213,14 +213,17 @@ void Engine::Run() { frameCount++; fpsUpdateTimer += deltaTime; - // Update window title with FPS and frame count every second + // Update window title with FPS and frame time every second if (fpsUpdateTimer >= 1.0f) { uint64_t framesSinceLastUpdate = frameCount - lastFPSUpdateFrame; currentFPS = framesSinceLastUpdate / fpsUpdateTimer; + // Average frame time in milliseconds over the last interval + double avgMs = (fpsUpdateTimer / static_cast(framesSinceLastUpdate)) * 1000.0; - // Update window title with frame count and FPS + // Update window title with frame count, FPS, and frame time std::string title = "Simple Engine - Frame: " + std::to_string(frameCount) + - " | FPS: " + std::to_string(static_cast(currentFPS)); + " | FPS: " + std::to_string(static_cast(currentFPS)) + + " | ms: " + std::to_string(static_cast(avgMs)); platform->SetWindowTitle(title); // Reset timer and frame counter for next update @@ -415,29 +418,26 @@ void Engine::Render() { } float Engine::CalculateDeltaTime() { - // Get current time - auto currentTime = static_cast(std::chrono::duration_cast( - std::chrono::high_resolution_clock::now().time_since_epoch() - ).count()); + // Get current time using a steady clock to avoid system time jumps + uint64_t currentTime = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() + ).count() + ); // Initialize lastFrameTime on first call if (lastFrameTime == 0) { lastFrameTime = currentTime; - return 0.016f; // Return ~16ms (60 FPS) for first frame + return 0.016f; // ~16ms as a sane initial guess } - // Calculate delta time + // Calculate delta time in milliseconds uint64_t delta = currentTime - lastFrameTime; // Update last frame time lastFrameTime = currentTime; - // Clamp delta time to reasonable values (prevent huge jumps) - if (delta > 10) { // Cap at 100ms (10 FPS minimum) - delta = 16; // Use 16ms instead - } - - return delta / 1000.0f; // Convert to seconds + return static_cast(delta) / 1000.0f; } void Engine::HandleResize(int width, int height) const { @@ -754,7 +754,7 @@ void Engine::HandleMouseHover(float mouseX, float mouseY) { } -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) // Android-specific implementation bool Engine::InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers) { // Create platform diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 897dc758..8d136ca3 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -141,7 +141,7 @@ class Engine { */ ImGuiSystem* GetImGuiSystem() const; -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) /** * @brief Initialize the engine for Android. * @param app The Android app. diff --git a/attachments/simple_engine/fetch_bistro_assets.bat b/attachments/simple_engine/fetch_bistro_assets.bat new file mode 100644 index 00000000..b8411b05 --- /dev/null +++ b/attachments/simple_engine/fetch_bistro_assets.bat @@ -0,0 +1,55 @@ +@echo off +setlocal enabledelayedexpansion + +REM Fetch the Bistro example assets into the desired assets directory. +REM Default target: assets\bistro at the repository root. +REM Usage: +REM fetch_bistro_assets.bat [target-dir] +REM Examples: +REM fetch_bistro_assets.bat +REM fetch_bistro_assets.bat attachments\simple_engine\Assets\bistro + +set REPO_SSH=git@github.com:gpx1000/bistro.git +set REPO_HTTPS=https://github.com/gpx1000/bistro.git + +if "%~1"=="" ( + set TARGET_DIR=assets\bistro +) else ( + set TARGET_DIR=%~1 +) + +REM Ensure parent directory exists +for %%I in ("%TARGET_DIR%") do set PARENT=%%~dpI +if not exist "%PARENT%" mkdir "%PARENT%" + +REM If directory exists and is a git repo, update it; otherwise clone it +if exist "%TARGET_DIR%\.git" ( + echo Updating existing bistro assets in %TARGET_DIR% + pushd "%TARGET_DIR%" + git pull --ff-only + popd +) else ( + echo Cloning bistro assets into %TARGET_DIR% + REM Try SSH first; fall back to HTTPS on failure + git clone --depth 1 "%REPO_SSH%" "%TARGET_DIR%" 2>nul + if %ERRORLEVEL% neq 0 ( + echo SSH clone failed, trying HTTPS + git clone --depth 1 "%REPO_HTTPS%" "%TARGET_DIR%" + ) +) + +REM If git-lfs is available, ensure LFS content is pulled +where git >nul 2>nul +if %ERRORLEVEL%==0 ( + pushd "%TARGET_DIR%" + git lfs version >nul 2>nul + if %ERRORLEVEL%==0 ( + git lfs install --local >nul 2>nul + git lfs pull + ) + popd +) + +echo Bistro assets ready at: %TARGET_DIR% +endlocal +exit /b 0 diff --git a/attachments/simple_engine/fetch_bistro_assets.sh b/attachments/simple_engine/fetch_bistro_assets.sh new file mode 100755 index 00000000..9580149c --- /dev/null +++ b/attachments/simple_engine/fetch_bistro_assets.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fetch the Bistro example assets into the desired assets directory. +# Default target: assets/bistro at the repository root. +# Usage: +# ./fetch_bistro_assets.sh [target-dir] +# Example: +# ./fetch_bistro_assets.sh # clones to assets/bistro + +REPO_SSH="git@github.com:gpx1000/bistro.git" +REPO_HTTPS="https://github.com/gpx1000/bistro.git" +TARGET_DIR="${1:-assets/bistro}" + +mkdir -p "$(dirname "${TARGET_DIR}")" + +# If directory exists and is a git repo, update it; otherwise clone it. +if [ -d "${TARGET_DIR}/.git" ]; then + echo "Updating existing bistro assets in ${TARGET_DIR}" + git -C "${TARGET_DIR}" pull --ff-only +else + echo "Cloning bistro assets into ${TARGET_DIR}" + # Try SSH first; if it fails (e.g., no SSH key), fall back to HTTPS. + if git clone --depth 1 "${REPO_SSH}" "${TARGET_DIR}" 2>/dev/null; then + : + else + echo "SSH clone failed, trying HTTPS" + git clone --depth 1 "${REPO_HTTPS}" "${TARGET_DIR}" + fi +fi + +# If git-lfs is available, ensure LFS content is pulled +if command -v git >/dev/null 2>&1 && git -C "${TARGET_DIR}" lfs version >/dev/null 2>&1; then + git -C "${TARGET_DIR}" lfs install --local >/dev/null 2>&1 || true + git -C "${TARGET_DIR}" lfs pull || true +fi + +echo "Bistro assets ready at: ${TARGET_DIR}" diff --git a/attachments/simple_engine/install_dependencies_linux.sh b/attachments/simple_engine/install_dependencies_linux.sh index 62cc7b55..e82daebc 100755 --- a/attachments/simple_engine/install_dependencies_linux.sh +++ b/attachments/simple_engine/install_dependencies_linux.sh @@ -51,8 +51,7 @@ install_ubuntu_debian() { libglfw3-dev \ libglm-dev \ libopenal-dev \ - libktx-dev \ - libstb-dev + libktx-dev # Install Slang compiler (for shader compilation) echo "Installing Slang compiler..." @@ -84,7 +83,7 @@ install_fedora_rhel() { openal-soft-devel # Note: Some packages might need to be built from source on RHEL/CentOS - echo "Note: Some dependencies (libktx, libstb, tinygltf) may need to be built from source" + echo "Note: Some dependencies (libktx, tinygltf) may need to be built from source" echo "Please refer to the project documentation for manual installation instructions" } @@ -107,9 +106,9 @@ install_arch() { # Install AUR packages (requires yay or another AUR helper) if command -v yay &> /dev/null; then - yay -S --noconfirm libktx stb + yay -S --noconfirm libktx else - echo "Note: Please install yay or another AUR helper to install libktx and stb packages" + echo "Note: Please install yay or another AUR helper to install libktx packages" echo "Alternatively, build these dependencies from source" fi } diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index e74ec088..b1d67e5c 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -132,13 +132,6 @@ if %errorLevel% neq 0 ( echo Warning: Failed to install KTX via vcpkg ) -REM Install STB -echo Installing STB... -vcpkg install stb:x64-windows -if %errorLevel% neq 0 ( - echo Warning: Failed to install STB via vcpkg -) - REM Install tinygltf echo Installing tinygltf... vcpkg install tinygltf:x64-windows diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index 238618da..4225249f 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -38,7 +38,7 @@ void SetupScene(Engine* engine) { LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); } -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) /** * @brief Android entry point. * @param app The Android app. diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp index a00a4037..13d8ec64 100644 --- a/attachments/simple_engine/memory_pool.cpp +++ b/attachments/simple_engine/memory_pool.cpp @@ -37,19 +37,21 @@ bool MemoryPool::initialize() { ); // Uniform buffer pool: Small allocations, host-visible + // Use 64-byte alignment to match nonCoherentAtomSize and prevent validation errors configurePool( PoolType::UNIFORM_BUFFER, 4 * 1024 * 1024, // 4MB blocks - 256, // 256B allocation units + 64, // 64B allocation units (aligned to nonCoherentAtomSize) vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, 4 // Max 4 blocks (16MB total) ); // Staging buffer pool: Variable allocations, host-visible + // Use 64-byte alignment to match nonCoherentAtomSize and prevent validation errors configurePool( PoolType::STAGING_BUFFER, 16 * 1024 * 1024, // 16MB blocks - 1024, // 1KB allocation units + 64, // 64B allocation units (aligned to nonCoherentAtomSize) vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, 4 // Max 4 blocks (64MB total) ); @@ -311,14 +313,16 @@ std::pair> MemoryPool: // Determine a pool type based on usage and properties PoolType poolType = PoolType::VERTEX_BUFFER; - if (usage & vk::BufferUsageFlagBits::eVertexBuffer) { + + // Check for host-visible requirements first (for instance buffers and staging) + if (properties & vk::MemoryPropertyFlagBits::eHostVisible) { + poolType = PoolType::STAGING_BUFFER; + } else if (usage & vk::BufferUsageFlagBits::eVertexBuffer) { poolType = PoolType::VERTEX_BUFFER; } else if (usage & vk::BufferUsageFlagBits::eIndexBuffer) { poolType = PoolType::INDEX_BUFFER; } else if (usage & vk::BufferUsageFlagBits::eUniformBuffer) { poolType = PoolType::UNIFORM_BUFFER; - } else if (properties & vk::MemoryPropertyFlagBits::eHostVisible) { - poolType = PoolType::STAGING_BUFFER; } // Create the buffer diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index af9bdc0f..6e2eef5d 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -5,98 +5,20 @@ // Most of the MeshComponent class implementation is in the header file // This file is mainly for any methods that might need additional implementation -void MeshComponent::CreateQuad(float width, float height, const glm::vec3& color) { - float halfWidth = width * 0.5f; - float halfHeight = height * 0.5f; - - // Quad facing forward (positive Z direction) - glm::vec3 normal = glm::vec3(0.0f, 0.0f, 1.0f); - glm::vec4 tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - - vertices = { - { {-halfWidth, -halfHeight, 0.0f}, normal, {0.0f, 0.0f}, tangent }, - { { halfWidth, -halfHeight, 0.0f}, normal, {1.0f, 0.0f}, tangent }, - { { halfWidth, halfHeight, 0.0f}, normal, {1.0f, 1.0f}, tangent }, - { {-halfWidth, halfHeight, 0.0f}, normal, {0.0f, 1.0f}, tangent } - }; - - indices = { - 0, 1, 2, - 2, 3, 0 - }; -} - -void MeshComponent::CreateCube(float size, const glm::vec3& color) { - float halfSize = size * 0.5f; - - vertices = { - // Front face (normal: +Z, tangent: +X) - { {-halfSize, -halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, -halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { {-halfSize, halfSize, halfSize}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - - // Back face (normal: -Z, tangent: -X) - { {-halfSize, -halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {1.0f, 0.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, - { {-halfSize, halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {0.0f, 1.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, -halfSize, -halfSize}, {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f, 1.0f} }, - - // Top face (normal: +Y, tangent: +X) - { {-halfSize, halfSize, -halfSize}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { {-halfSize, halfSize, halfSize}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, halfSize, halfSize}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, halfSize, -halfSize}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - - // Bottom face (normal: -Y, tangent: +X) - { {-halfSize, -halfSize, -halfSize}, {0.0f, -1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, -halfSize, -halfSize}, {0.0f, -1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { { halfSize, -halfSize, halfSize}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - { {-halfSize, -halfSize, halfSize}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f} }, - - // Right face (normal: +X, tangent: -Z) - { { halfSize, -halfSize, -halfSize}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, - { { halfSize, halfSize, -halfSize}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, - { { halfSize, halfSize, halfSize}, {1.0f, 0.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, - { { halfSize, -halfSize, halfSize}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, -1.0f, 1.0f} }, - - // Left face (normal: -X, tangent: +Z) - { {-halfSize, -halfSize, -halfSize}, {-1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f, 1.0f} }, - { {-halfSize, -halfSize, halfSize}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f, 1.0f} }, - { {-halfSize, halfSize, halfSize}, {-1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f} }, - { {-halfSize, halfSize, -halfSize}, {-1.0f, 0.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f} } - }; - - indices = { - // Front face - 0, 1, 2, 2, 3, 0, - // Back face - 4, 5, 6, 6, 7, 4, - // Top face - 8, 9, 10, 10, 11, 8, - // Bottom face - 12, 13, 14, 14, 15, 12, - // Right face - 16, 17, 18, 18, 19, 16, - // Left face - 20, 21, 22, 22, 23, 20 - }; -} - void MeshComponent::CreateSphere(float radius, const glm::vec3& color, int segments) { vertices.clear(); indices.clear(); // Generate sphere vertices using parametric equations for (int lat = 0; lat <= segments; ++lat) { - float theta = lat * M_PI / segments; // Latitude angle (0 to PI) - float sinTheta = sin(theta); - float cosTheta = cos(theta); + auto theta = static_cast(lat * M_PI / segments); // Latitude angle (0 to PI) + float sinTheta = sinf(theta); + float cosTheta = cosf(theta); for (int lon = 0; lon <= segments; ++lon) { - float phi = lon * 2.0f * M_PI / segments; // Longitude angle (0 to 2*PI) - float sinPhi = sin(phi); - float cosPhi = cos(phi); + auto phi = static_cast(lon * 2.0 * M_PI / segments); // Longitude angle (0 to 2*PI) + float sinPhi = sinf(phi); + float cosPhi = cosf(phi); // Calculate position glm::vec3 position = { @@ -110,8 +32,8 @@ void MeshComponent::CreateSphere(float radius, const glm::vec3& color, int segme // Texture coordinates glm::vec2 texCoord = { - (float)lon / segments, - (float)lat / segments + static_cast(lon) / static_cast(segments), + static_cast(lat) / static_cast(segments) }; // Calculate tangent (derivative with respect to longitude) @@ -147,6 +69,8 @@ void MeshComponent::CreateSphere(float radius, const glm::vec3& color, int segme indices.push_back(next + 1); } } + + RecomputeLocalAABB(); } void MeshComponent::LoadFromModel(const Model* model) { @@ -157,4 +81,6 @@ void MeshComponent::LoadFromModel(const Model* model) { // Copy vertex and index data from the model vertices = model->GetVertices(); indices = model->GetIndices(); + + RecomputeLocalAABB(); } diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index d73b9724..00d382b4 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -9,6 +9,185 @@ #include "component.h" +/** + * @brief Structure representing per-instance data for instanced rendering. + * Using explicit float vectors instead of matrices for better control over GPU data layout. + */ +struct InstanceData { + // Model matrix as glm::mat4 (4x4) + glm::mat4 modelMatrix{}; + + // Normal matrix as glm::mat3x4 (3 columns of vec4: xyz = normal matrix columns, w unused) + glm::mat3x4 normalMatrix{}; + + InstanceData() { + // Initialize as identity matrices + modelMatrix = glm::mat4(1.0f); + normalMatrix[0] = glm::vec4(1.0f, 0.0f, 0.0f, 0.0f); + normalMatrix[1] = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f); + normalMatrix[2] = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); + } + + explicit InstanceData(const glm::mat4& transform, uint32_t matIndex = 0) { + // Store model matrix directly + modelMatrix = transform; + + // Calculate normal matrix (inverse transpose of upper-left 3x3) + glm::mat3 normalMat3 = glm::transpose(glm::inverse(glm::mat3(transform))); + normalMatrix[0] = glm::vec4(normalMat3[0], 0.0f); + normalMatrix[1] = glm::vec4(normalMat3[1], 0.0f); + normalMatrix[2] = glm::vec4(normalMat3[2], 0.0f); + + // Note: matIndex parameter ignored since materialIndex field was removed + } + + // Helper methods for backward compatibility + [[nodiscard]] glm::mat4 getModelMatrix() const { + return modelMatrix; + } + + void setModelMatrix(const glm::mat4& matrix) { + modelMatrix = matrix; + + // Also update normal matrix when model matrix changes + glm::mat3 normalMat3 = glm::transpose(glm::inverse(glm::mat3(matrix))); + normalMatrix[0] = glm::vec4(normalMat3[0], 0.0f); + normalMatrix[1] = glm::vec4(normalMat3[1], 0.0f); + normalMatrix[2] = glm::vec4(normalMat3[2], 0.0f); + } + + [[nodiscard]] glm::mat3 getNormalMatrix() const { + return { + glm::vec3(normalMatrix[0]), + glm::vec3(normalMatrix[1]), + glm::vec3(normalMatrix[2]) + }; + } + + + static vk::VertexInputBindingDescription getBindingDescription() { + vk::VertexInputBindingDescription bindingDescription( + 1, // binding (binding 1 for instance data) + sizeof(InstanceData), // stride + vk::VertexInputRate::eInstance // inputRate + ); + return bindingDescription; + } + + static std::array getAttributeDescriptions() { + constexpr uint32_t modelBase = offsetof(InstanceData, modelMatrix); + constexpr uint32_t normalBase = offsetof(InstanceData, normalMatrix); + constexpr uint32_t vec4Size = sizeof(glm::vec4); + std::array attributeDescriptions = { + // Model matrix columns (locations 4-7) + vk::VertexInputAttributeDescription{ + .location = 4, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 0u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 5, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 1u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 6, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 2u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 7, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 3u * vec4Size + }, + // Normal matrix columns (locations 8-10) + vk::VertexInputAttributeDescription{ + .location = 8, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 0u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 9, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 1u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 10, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 2u * vec4Size + } + }; + return attributeDescriptions; + } + + // Get all attribute descriptions for model matrix (4 vec4s) + static std::array getModelMatrixAttributeDescriptions() { + constexpr uint32_t modelBase = offsetof(InstanceData, modelMatrix); + constexpr uint32_t vec4Size = sizeof(glm::vec4); + std::array attributeDescriptions = { + vk::VertexInputAttributeDescription{ + .location = 4, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 0u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 5, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 1u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 6, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 2u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 7, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 3u * vec4Size + } + }; + return attributeDescriptions; + } + + // Get all attribute descriptions for normal matrix (3 vec4s) + static std::array getNormalMatrixAttributeDescriptions() { + constexpr uint32_t normalBase = offsetof(InstanceData, normalMatrix); + constexpr uint32_t vec4Size = sizeof(glm::vec4); + std::array attributeDescriptions = { + vk::VertexInputAttributeDescription{ + .location = 8, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 0u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 9, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 1u * vec4Size + }, + vk::VertexInputAttributeDescription{ + .location = 10, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 2u * vec4Size + } + }; + return attributeDescriptions; + } +}; + /** * @brief Structure representing a vertex in a mesh. */ @@ -73,6 +252,11 @@ class MeshComponent : public Component { std::vector vertices; std::vector indices; + // Cached local-space AABB + glm::vec3 localAABBMin{0.0f}; + glm::vec3 localAABBMax{0.0f}; + bool localAABBValid = false; + // All PBR texture paths for this mesh std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility std::string baseColorTexturePath; // Base color (albedo) texture @@ -81,23 +265,50 @@ class MeshComponent : public Component { std::string occlusionTexturePath; // Ambient occlusion texture std::string emissiveTexturePath; // Emissive texture - // Vulkan resources will be managed by the renderer + // Instancing support + std::vector instances; // Instance data for instanced rendering + bool isInstanced = false; // Flag to indicate if this mesh uses instancing + + // The renderer will manage Vulkan resources // This component only stores the data public: /** - * @brief Constructor with optional name. + * @brief Constructor with an optional name. * @param componentName The name of the component. */ explicit MeshComponent(const std::string& componentName = "MeshComponent") : Component(componentName) {} + // Local AABB utilities + void RecomputeLocalAABB() { + if (vertices.empty()) { + localAABBMin = glm::vec3(0.0f); + localAABBMax = glm::vec3(0.0f); + localAABBValid = false; + return; + } + glm::vec3 minB = vertices[0].position; + glm::vec3 maxB = vertices[0].position; + for (const auto& v : vertices) { + minB = glm::min(minB, v.position); + maxB = glm::max(maxB, v.position); + } + localAABBMin = minB; + localAABBMax = maxB; + localAABBValid = true; + } + [[nodiscard]] bool HasLocalAABB() const { return localAABBValid; } + [[nodiscard]] glm::vec3 GetLocalAABBMin() const { return localAABBMin; } + [[nodiscard]] glm::vec3 GetLocalAABBMax() const { return localAABBMax; } + /** * @brief Set the vertices of the mesh. * @param newVertices The new vertices. */ void SetVertices(const std::vector& newVertices) { vertices = newVertices; + RecomputeLocalAABB(); } /** @@ -155,21 +366,6 @@ class MeshComponent : public Component { [[nodiscard]] const std::string& GetOcclusionTexturePath() const { return occlusionTexturePath; } [[nodiscard]] const std::string& GetEmissiveTexturePath() const { return emissiveTexturePath; } - /** - * @brief Create a simple quad mesh. - * @param width The width of the quad. - * @param height The height of the quad. - * @param color The color of the quad. - */ - void CreateQuad(float width = 1.0f, float height = 1.0f, const glm::vec3& color = glm::vec3(1.0f)); - - /** - * @brief Create a simple cube mesh. - * @param size The size of the cube. - * @param color The color of the cube. - */ - void CreateCube(float size = 1.0f, const glm::vec3& color = glm::vec3(1.0f)); - /** * @brief Create a simple sphere mesh. * @param radius The radius of the sphere. @@ -183,4 +379,83 @@ class MeshComponent : public Component { * @param model Pointer to the model to load from. */ void LoadFromModel(const class Model* model); + + // Instancing methods + + /** + * @brief Add an instance with the given transform matrix. + * @param transform The transform matrix for this instance. + * @param materialIndex The material index for this instance (default: 0). + */ + void AddInstance(const glm::mat4& transform, uint32_t materialIndex = 0) { + instances.emplace_back(transform, materialIndex); + isInstanced = instances.size() > 1; + } + + /** + * @brief Set all instances at once. + * @param newInstances Vector of instance data. + */ + void SetInstances(const std::vector& newInstances) { + instances = newInstances; + isInstanced = instances.size() > 1; + } + + /** + * @brief Get all instance data. + * @return Reference to the instances vector. + */ + [[nodiscard]] const std::vector& GetInstances() const { + return instances; + } + + /** + * @brief Get the number of instances. + * @return Number of instances (0 if not instanced, >= 1 if instanced). + */ + [[nodiscard]] size_t GetInstanceCount() const { + return instances.size(); + } + + /** + * @brief Check if this mesh uses instancing. + * @return True if instanced (more than 1 instance), false otherwise. + */ + [[nodiscard]] bool IsInstanced() const { + return isInstanced; + } + + /** + * @brief Clear all instances and disable instancing. + */ + void ClearInstances() { + instances.clear(); + isInstanced = false; + } + + /** + * @brief Update a specific instance's transform. + * @param index The index of the instance to update. + * @param transform The new transform matrix. + * @param materialIndex The new material index (optional). + */ + void UpdateInstance(size_t index, const glm::mat4& transform, uint32_t materialIndex = 0) { + if (index < instances.size()) { + instances[index] = InstanceData(transform, materialIndex); + } + } + + /** + * @brief Get a specific instance's data. + * @param index The index of the instance. + * @return Reference to the instance data, or first instance if the index is out of bounds. + */ + [[nodiscard]] const InstanceData& GetInstance(size_t index) const { + if (index < instances.size()) { + return instances[index]; + } + // Return the first instance or default if empty + static const InstanceData defaultInstance; + return instances.empty() ? defaultInstance : instances[0]; + } }; diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index e25e4d14..52a6475e 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -4,13 +4,44 @@ #include #include #include +#include #include -// Include stb_image for proper texture loading (implementation is in renderer_resources.cpp) -#include +// KTX2 decoding for GLTF images +#include + +// Helper: load KTX2 file from disk into RGBA8 CPU buffer +static bool LoadKTX2FileToRGBA(const std::string& filePath, std::vector& outData, int& width, int& height, int& channels) { + ktxTexture2* ktxTex = nullptr; + KTX_error_code result = ktxTexture2_CreateFromNamedFile(filePath.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result != KTX_SUCCESS || !ktxTex) { + return false; + } + bool needsTranscode = ktxTexture2_NeedsTranscoding(ktxTex); + if (needsTranscode) { + result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); + if (result != KTX_SUCCESS) { + ktxTexture_Destroy((ktxTexture*)ktxTex); + return false; + } + } + width = static_cast(ktxTex->baseWidth); + height = static_cast(ktxTex->baseHeight); + channels = 4; + ktx_size_t offset; + ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); + const uint8_t* levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; + size_t levelSize = needsTranscode ? static_cast(width) * static_cast(height) * 4 + : ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); + outData.resize(levelSize); + std::memcpy(outData.data(), levelData, levelSize); + ktxTexture_Destroy((ktxTexture*)ktxTex); + return true; +} // Emissive scaling factor to convert from Blender units to engine units #define EMISSIVE_SCALE_FACTOR (1.0f / 638.0f) +#define LIGHT_SCALE_FACTOR (1.0f / 2.0f) // The sun is way too bright ModelLoader::~ModelLoader() { // Destructor implementation @@ -18,8 +49,8 @@ ModelLoader::~ModelLoader() { materials.clear(); } -bool ModelLoader::Initialize(Renderer* renderer) { - this->renderer = renderer; +bool ModelLoader::Initialize(Renderer* _renderer) { + renderer = _renderer; if (!renderer) { std::cerr << "ModelLoader::Initialize: Renderer is null" << std::endl; @@ -51,43 +82,6 @@ Model* ModelLoader::LoadGLTF(const std::string& filename) { return models[filename].get(); } -Model* ModelLoader::LoadGLTFWithPBR(const std::string& filename, - const std::string& albedoMap, - const std::string& normalMap, - const std::string& metallicRoughnessMap, - const std::string& aoMap, - const std::string& emissiveMap) { - // Check if the model is already loaded - auto it = models.find(filename); - if (it != models.end()) { - return it->second.get(); - } - - // Create a new model - auto model = std::make_unique(filename); - - // Parse the GLTF file - if (!ParseGLTF(filename, model.get())) { - std::cerr << "ModelLoader::LoadGLTFWithPBR: Failed to parse GLTF file: " << filename << std::endl; - return nullptr; - } - - // Create a PBR material - auto material = std::make_unique(filename + "_material"); - - // Load PBR textures - if (!LoadPBRTextures(material.get(), albedoMap, normalMap, metallicRoughnessMap, aoMap, emissiveMap)) { - std::cerr << "ModelLoader::LoadGLTFWithPBR: Failed to load PBR textures for model: " << filename << std::endl; - } - - // Store the material - materials[material->GetName()] = std::move(material); - - // Store the model - models[filename] = std::move(model); - - return models[filename].get(); -} Model* ModelLoader::GetModel(const std::string& name) { auto it = models.find(name); @@ -97,41 +91,14 @@ Model* ModelLoader::GetModel(const std::string& name) { return nullptr; } -Material* ModelLoader::CreatePBRMaterial(const std::string& name, - const glm::vec3& albedo, - float metallic, - float roughness, - float ao, - const glm::vec3& emissive) { - // Check if the material already exists - auto it = materials.find(name); - if (it != materials.end()) { - return it->second.get(); - } - - // Create a new material - auto material = std::make_unique(name); - - // Set PBR properties - material->albedo = albedo; - material->metallic = metallic; - material->roughness = roughness; - material->ao = ao; - material->emissive = emissive * EMISSIVE_SCALE_FACTOR; - - // Store the material - materials[name] = std::move(material); - - std::cout << "PBR material created successfully: " << name << std::endl; - return materials[name].get(); -} bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cout << "Parsing GLTF file: " << filename << std::endl; - // Extract the directory path from the model file to use as base path for textures + // Extract the directory path from the model file to use as a base path for textures std::filesystem::path modelPath(filename); - std::string baseTexturePath = modelPath.parent_path().string(); + std::filesystem::path baseDir = std::filesystem::absolute(modelPath).parent_path(); + std::string baseTexturePath = baseDir.string(); if (!baseTexturePath.empty() && baseTexturePath.back() != '/') { baseTexturePath += "/"; } @@ -143,37 +110,45 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::string err; std::string warn; - // Set up a proper image loader callback using stb_image + // Set up image loader: prefer KTX2 via libktx; fallback to stb for other formats loader.SetImageLoader([](tinygltf::Image* image, const int image_idx, std::string* err, std::string* warn, int req_width, int req_height, const unsigned char* bytes, int size, void* user_data) -> bool { - // Use stb_image to decode the image data - int width, height, channels; - unsigned char* data = stbi_load_from_memory(bytes, size, &width, &height, &channels, 0); - - if (!data) { - if (err) { - *err = "Failed to load image with stb_image: " + std::string(stbi_failure_reason()); + // Try KTX2 first using libktx + ktxTexture2* ktxTex = nullptr; + KTX_error_code result = ktxTexture2_CreateFromMemory(bytes, size, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result == KTX_SUCCESS && ktxTex) { + bool needsTranscode = ktxTexture2_NeedsTranscoding(ktxTex); + if (needsTranscode) { + result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); + if (result != KTX_SUCCESS) { + if (err) *err = "Failed to transcode KTX2 image: " + std::to_string(result); + ktxTexture_Destroy((ktxTexture*)ktxTex); + return false; + } } - return false; + image->width = static_cast(ktxTex->baseWidth); + image->height = static_cast(ktxTex->baseHeight); + image->component = 4; + image->bits = 8; + image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + + ktx_size_t offset; + ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); + const uint8_t* levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; + size_t levelSize = needsTranscode ? static_cast(image->width) * static_cast(image->height) * 4 + : ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); + image->image.resize(levelSize); + std::memcpy(image->image.data(), levelData, levelSize); + ktxTexture_Destroy((ktxTexture*)ktxTex); + return true; } - // Set image properties - image->width = width; - image->height = height; - image->component = channels; - image->bits = 8; - image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; - - // Copy image data - size_t image_size = width * height * channels; - image->image.resize(image_size); - std::memcpy(image->image.data(), data, image_size); - - // Free stb_image data - stbi_image_free(data); - - return true; + // Non-KTX images not supported by this loader per project simplification + if (err) { + *err = "Non-KTX2 images are not supported by the custom image loader (use KTX2)."; + } + return false; }, nullptr); // Load the GLTF file @@ -204,6 +179,15 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { return false; } + // Test if generator is blender and apply the blender factor see the issue here: https://github.com/KhronosGroup/glTF/issues/2473 + if (gltfModel.asset.generator.find("blender") != std::string::npos) { + std::cout << "Blender generator detected, applying blender factor" << std::endl; + light_scale = EMISSIVE_SCALE_FACTOR; + } + + // Track loaded textures to prevent loading the same texture multiple times + std::set loadedTextures; + // Process materials first for (size_t i = 0; i < gltfModel.materials.size(); ++i) { const auto& gltfMaterial = gltfModel.materials[i]; @@ -218,6 +202,9 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { gltfMaterial.pbrMetallicRoughness.baseColorFactor[1], gltfMaterial.pbrMetallicRoughness.baseColorFactor[2] ); + if (gltfMaterial.pbrMetallicRoughness.baseColorFactor.size() >= 4) { + material->alpha = static_cast(gltfMaterial.pbrMetallicRoughness.baseColorFactor[3]); + } } material->metallic = static_cast(gltfMaterial.pbrMetallicRoughness.metallicFactor); material->roughness = static_cast(gltfMaterial.pbrMetallicRoughness.roughnessFactor); @@ -227,7 +214,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { gltfMaterial.emissiveFactor[0], gltfMaterial.emissiveFactor[1], gltfMaterial.emissiveFactor[2] - ) * EMISSIVE_SCALE_FACTOR; + ) * light_scale; } // Parse KHR_materials_emissive_strength extension @@ -235,11 +222,11 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { if (extensionIt != gltfMaterial.extensions.end()) { const tinygltf::Value& extension = extensionIt->second; if (extension.Has("emissiveStrength") && extension.Get("emissiveStrength").IsNumber()) { - material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()) * EMISSIVE_SCALE_FACTOR; + material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()) * light_scale; } } else { - // Default emissive strength is 1.0 according to GLTF spec, scaled for engine units - material->emissiveStrength = 1.0f * EMISSIVE_SCALE_FACTOR; + // Default emissive strength is 1.0, according to GLTF spec, scaled for engine units + material->emissiveStrength = 1.0f * light_scale; } @@ -248,35 +235,51 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; if (texIndex < gltfModel.textures.size()) { const auto& texture = gltfModel.textures[texIndex]; + int imageIndex = -1; if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); + imageIndex = texture.source; + } else { + auto extIt = texture.extensions.find("KHR_texture_basisu"); + if (extIt != texture.extensions.end()) { + const tinygltf::Value& ext = extIt->second; + if (ext.Has("source") && ext.Get("source").IsInt()) { + int src = ext.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) { + imageIndex = src; + } + } + } + } + if (imageIndex >= 0) { + std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); material->albedoTexturePath = textureId; // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; + const auto& image = gltfModel.images[imageIndex]; std::cout << " Image data size: " << image.image.size() << ", URI: " << image.uri << std::endl; if (!image.image.empty()) { - // Load embedded texture data - std::cout << " Loading embedded base color texture: " << textureId << std::endl; + // Always use memory-based upload (KTX2 already decoded by SetImageLoader) if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Successfully loaded embedded base color texture: " << textureId << std::endl; + material->albedoTexturePath = textureId; + std::cout << " Loaded base color texture from memory: " << textureId << std::endl; } else { - std::cerr << " Failed to load embedded base color texture: " << textureId << std::endl; + std::cerr << " Failed to load base color texture from memory: " << textureId << std::endl; } } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - std::cout << " Loading external base color texture: " << texturePath << std::endl; - if (renderer->LoadTexture(texturePath)) { - // Update the material to use the external texture path instead of gltf_texture_X - material->albedoTexturePath = texturePath; - std::cout << " Successfully loaded external base color texture: " << texturePath << std::endl; + // Fallback: load a KTX2 file directly and upload from memory + std::vector data; + int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + material->albedoTexturePath = textureId; + std::cout << " Loaded base color KTX2 file: " << filePath << std::endl; } else { - std::cerr << " Failed to load external base color texture: " << texturePath << std::endl; + std::cerr << " Failed to load base color KTX2 file: " << filePath << std::endl; } } else { - std::cout << " No image data or URI available, skipping texture loading" << std::endl; + std::cerr << " Warning: No decoded image bytes for base color texture index " << texIndex << std::endl; } } } @@ -301,15 +304,17 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cerr << " Failed to load embedded metallic-roughness texture: " << textureId << std::endl; } } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the material to use the external texture path instead of gltf_texture_X - material->metallicRoughnessTexturePath = texturePath; - std::cout << " Successfully loaded external metallic-roughness texture: " << texturePath << std::endl; + // Fallback: load KTX2 from a file and upload to memory + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + std::cout << " Loaded metallic-roughness KTX2 file: " << filePath << std::endl; } else { - std::cerr << " Failed to load external metallic-roughness texture: " << texturePath << std::endl; + std::cerr << " Failed to load metallic-roughness KTX2 file: " << filePath << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; } } } @@ -319,31 +324,49 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { int texIndex = gltfMaterial.normalTexture.index; if (texIndex < gltfModel.textures.size()) { const auto& texture = gltfModel.textures[texIndex]; + int imageIndex = -1; if (texture.source >= 0 && texture.source < gltfModel.images.size()) { + imageIndex = texture.source; + } else { + auto extIt = texture.extensions.find("KHR_texture_basisu"); + if (extIt != texture.extensions.end()) { + const tinygltf::Value& ext = extIt->second; + if (ext.Has("source") && ext.Get("source").IsInt()) { + int src = ext.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) { + imageIndex = src; + } + } + } + } + if (imageIndex >= 0) { std::string textureId = "gltf_texture_" + std::to_string(texIndex); material->normalTexturePath = textureId; // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; + const auto& image = gltfModel.images[imageIndex]; if (!image.image.empty()) { - // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Successfully loaded embedded normal texture: " << textureId + material->normalTexturePath = textureId; + std::cout << " Loaded normal texture from memory: " << textureId << " (" << image.width << "x" << image.height << ")" << std::endl; } else { - std::cerr << " Failed to load embedded normal texture: " << textureId << std::endl; + std::cerr << " Failed to load normal texture from memory: " << textureId << std::endl; } } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the material to use the external texture path instead of gltf_texture_X - material->normalTexturePath = texturePath; - std::cout << " Successfully loaded external normal texture: " << texturePath << std::endl; + // Fallback: load KTX2 from a file and upload to memory + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + material->normalTexturePath = textureId; + std::cout << " Loaded normal KTX2 file: " << filePath << std::endl; } else { - std::cerr << " Failed to load external normal texture: " << texturePath << std::endl; + std::cerr << " Failed to load normal KTX2 file: " << filePath << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; } } } @@ -369,15 +392,18 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cerr << " Failed to load embedded occlusion texture: " << textureId << std::endl; } } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the material to use the external texture path instead of gltf_texture_X - material->occlusionTexturePath = texturePath; - std::cout << " Successfully loaded external occlusion texture: " << texturePath << std::endl; + // Fallback: load KTX2 from a file and upload to memory + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + material->occlusionTexturePath = textureId; + std::cout << " Loaded occlusion KTX2 file: " << filePath << std::endl; } else { - std::cerr << " Failed to load external occlusion texture: " << texturePath << std::endl; + std::cerr << " Failed to load occlusion KTX2 file: " << filePath << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; } } } @@ -403,15 +429,18 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cerr << " Failed to load embedded emissive texture: " << textureId << std::endl; } } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the material to use the external texture path instead of gltf_texture_X - material->emissiveTexturePath = texturePath; - std::cout << " Successfully loaded external emissive texture: " << texturePath << std::endl; + // Fallback: load KTX2 from a file and upload to memory + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + material->emissiveTexturePath = textureId; + std::cout << " Loaded emissive KTX2 file: " << filePath << std::endl; } else { - std::cerr << " Failed to load external emissive texture: " << texturePath << std::endl; + std::cerr << " Failed to load emissive KTX2 file: " << filePath << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for emissive texture index " << texIndex << std::endl; } } } @@ -421,7 +450,154 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { materials[material->GetName()] = std::move(material); } - // Process cameras from GLTF file + // Handle KHR_materials_pbrSpecularGlossiness.diffuseTexture for baseColor when still missing + for (size_t i = 0; i < gltfModel.materials.size(); ++i) { + const auto &gltfMaterial = gltfModel.materials[i]; + std::string matName = gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name; + auto matIt = materials.find(matName); + if (matIt == materials.end()) continue; + Material* mat = matIt->second.get(); + if (!mat || !mat->albedoTexturePath.empty()) continue; + auto extIt = gltfMaterial.extensions.find("KHR_materials_pbrSpecularGlossiness"); + if (extIt != gltfMaterial.extensions.end()) { + const tinygltf::Value &ext = extIt->second; + if (ext.Has("diffuseTexture") && ext.Get("diffuseTexture").IsObject()) { + const auto &diffObj = ext.Get("diffuseTexture"); + if (diffObj.Has("index") && diffObj.Get("index").IsInt()) { + int texIndex = diffObj.Get("index").Get(); + if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) { + const auto &texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) { + imageIndex = texture.source; + } else { + auto extBasis = texture.extensions.find("KHR_texture_basisu"); + if (extBasis != texture.extensions.end()) { + const tinygltf::Value &e = extBasis->second; + if (e.Has("source") && e.Get("source").IsInt()) { + int src = e.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) imageIndex = src; + } + } + } + if (imageIndex >= 0) { + const auto &image = gltfModel.images[imageIndex]; + std::string texIdOrPath; + if (!image.uri.empty()) { + texIdOrPath = baseTexturePath + image.uri; + // Try loading from a KTX2 file on disk first + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(texIdOrPath, data, w, h, c) && renderer->LoadTextureFromMemory(texIdOrPath, data.data(), w, h, c)) { + mat->albedoTexturePath = texIdOrPath; + std::cout << " Loaded base color KTX2 file (KHR_specGloss): " << texIdOrPath << std::endl; + } + } + if (mat->albedoTexturePath.empty() && !image.image.empty()) { + // Upload embedded image data (already decoded via our image loader when KTX2) + texIdOrPath = "gltf_baseColor_" + std::to_string(texIndex); + if (renderer->LoadTextureFromMemory(texIdOrPath, image.image.data(), image.width, image.height, image.component)) { + mat->albedoTexturePath = texIdOrPath; + std::cout << " Loaded base color texture from memory (KHR_specGloss): " << texIdOrPath << std::endl; + } + } + } + } + } + } + } + } + + // Heuristic pass: fill missing baseColor (albedo) by deriving from normal map filenames + // Many Bistro materials have no baseColorTexture index. When that happens, try inferring + // the base color from the normal map by replacing common suffixes like _ddna -> _d/_c/_diffuse/_basecolor/_albedo. + for (auto& material : materials | std::views::values) { + Material* mat = material.get(); + if (!mat) continue; + if (!mat->albedoTexturePath.empty()) continue; // already set + // Only attempt if we have an external normal texture path to derive from + if (mat->normalTexturePath.empty()) continue; + const std::string &normalPath = mat->normalTexturePath; + // Skip embedded IDs like gltf_* which were already handled by memory uploads + if (normalPath.rfind("gltf_", 0) == 0) continue; + + std::string candidateBase = normalPath; + std::string normalLower = candidateBase; + for (auto &ch : normalLower) ch = static_cast(std::tolower(static_cast(ch))); + size_t pos = normalLower.find("_ddna"); + if (pos == std::string::npos) { + // Try a few additional normal suffixes seen in the wild + pos = normalLower.find("_n"); + } + if (pos != std::string::npos) { + static const char* suffixes[] = {"_d", "_c", "_cm", "_diffuse", "_basecolor", "_albedo"}; + for (const char* suf : suffixes) { + std::string cand = candidateBase; + cand.replace(pos, normalLower[pos]=='_' && normalLower.compare(pos, 5, "_ddna")==0 ? 5 : 2, suf); + // Ensure the file exists before attempting to load + if (std::filesystem::exists(cand)) { + // Load KTX2 (or KTX) file via libktx then upload from memory + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(cand, data, w, h, c)) { + if (renderer->LoadTextureFromMemory(cand, data.data(), w, h, c)) { + mat->albedoTexturePath = cand; + std::cout << " Derived base color from normal sibling: " << cand << std::endl; + break; + } + } + } + } + } + } + + // Secondary heuristic: scan glTF images for base color by material-name match when still missing + for (auto &entry : materials) { + Material* mat = entry.second.get(); + if (!mat) continue; + if (!mat->albedoTexturePath.empty()) continue; // already resolved + // Try to find an image URI that looks like the base color for this material + std::string materialNameLower = entry.first; + std::ranges::transform(materialNameLower, materialNameLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + for (const auto &image : gltfModel.images) { + if (image.uri.empty()) continue; + std::string imageUri = image.uri; + std::string imageUriLower = imageUri; + std::ranges::transform(imageUriLower, imageUriLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + bool looksBase = imageUriLower.find("basecolor") != std::string::npos || + imageUriLower.find("albedo") != std::string::npos || + imageUriLower.find("diffuse") != std::string::npos; + if (!looksBase) continue; + bool nameMatches = imageUriLower.find(materialNameLower) != std::string::npos; + if (!nameMatches) { + // Best-effort: try prefix of image name before '_' against material name + size_t underscore = imageUriLower.find('_'); + if (underscore != std::string::npos) { + std::string prefix = imageUriLower.substr(0, underscore); + nameMatches = materialNameLower.find(prefix) != std::string::npos; + } + } + if (!nameMatches) continue; + + std::string textureId = baseTexturePath + imageUri; // use path string as ID for cache + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { + mat->albedoTexturePath = textureId; + std::cout << " Loaded base color texture from memory (by name): " << textureId << std::endl; + break; + } + } else { + // Fallback: load KTX2 file from disk + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(textureId, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + mat->albedoTexturePath = textureId; + std::cout << " Loaded base color KTX2 file (by name): " << textureId << std::endl; + break; + } + } + } + } + + // Process cameras from the GLTF file if (!gltfModel.cameras.empty()) { std::cout << "Found " << gltfModel.cameras.size() << " camera(s) in GLTF file" << std::endl; @@ -454,8 +630,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } // Find the node that uses this camera to get transform information - for (size_t nodeIdx = 0; nodeIdx < gltfModel.nodes.size(); ++nodeIdx) { - const auto& node = gltfModel.nodes[nodeIdx]; + for (const auto & node : gltfModel.nodes) { if (node.camera == static_cast(i)) { // Extract transform from node if (node.translation.size() == 3) { @@ -485,15 +660,133 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } } - // Group primitives by material to create separate meshes for each material - std::map> materialVertices; - std::map> materialIndices; - std::map materialNames; + // Process scene hierarchy to get node transforms for meshes + std::map> meshInstanceTransforms; // Map from mesh index to all instance transforms + + // Helper function to calculate transform matrix from the GLTF node + auto calculateNodeTransform = [](const tinygltf::Node& node) -> glm::mat4 { + glm::mat4 transform; + + // Apply matrix if present + if (node.matrix.size() == 16) { + // GLTF matrices are column-major, the same as GLM + transform = glm::mat4( + node.matrix[0], node.matrix[1], node.matrix[2], node.matrix[3], + node.matrix[4], node.matrix[5], node.matrix[6], node.matrix[7], + node.matrix[8], node.matrix[9], node.matrix[10], node.matrix[11], + node.matrix[12], node.matrix[13], node.matrix[14], node.matrix[15] + ); + } else { + // Build transform from TRS components + glm::mat4 translation = glm::mat4(1.0f); + glm::mat4 rotation = glm::mat4(1.0f); + glm::mat4 scale = glm::mat4(1.0f); + + // Translation + if (node.translation.size() == 3) { + translation = glm::translate(glm::mat4(1.0f), glm::vec3( + static_cast(node.translation[0]), + static_cast(node.translation[1]), + static_cast(node.translation[2]) + )); + } + + // Rotation (quaternion) + if (node.rotation.size() == 4) { + glm::quat quat( + static_cast(node.rotation[3]), // w + static_cast(node.rotation[0]), // x + static_cast(node.rotation[1]), // y + static_cast(node.rotation[2]) // z + ); + rotation = glm::mat4_cast(quat); + } + + // Scale + if (node.scale.size() == 3) { + scale = glm::scale(glm::mat4(1.0f), glm::vec3( + static_cast(node.scale[0]), + static_cast(node.scale[1]), + static_cast(node.scale[2]) + )); + } + + // Combine: T * R * S + transform = translation * rotation * scale; + } + + return transform; + }; + + // Recursive function to traverse scene hierarchy + std::function traverseNode = [&](int nodeIndex, const glm::mat4& parentTransform) { + if (nodeIndex < 0 || nodeIndex >= gltfModel.nodes.size()) { + return; + } + + const tinygltf::Node& node = gltfModel.nodes[nodeIndex]; + + // Calculate this node's transform + glm::mat4 nodeTransform = calculateNodeTransform(node); + glm::mat4 worldTransform = parentTransform * nodeTransform; + + // If this node has a mesh, add the transform to the instances list + if (node.mesh >= 0 && node.mesh < gltfModel.meshes.size()) { + meshInstanceTransforms[node.mesh].push_back(worldTransform); + } + + // Recursively process children + for (int childIndex : node.children) { + traverseNode(childIndex, worldTransform); + } + }; + + // Process all scenes (typically there's only one default scene) + if (!gltfModel.scenes.empty()) { + int defaultScene = gltfModel.defaultScene >= 0 ? gltfModel.defaultScene : 0; + if (defaultScene < gltfModel.scenes.size()) { + const tinygltf::Scene& scene = gltfModel.scenes[defaultScene]; + + // Traverse all root nodes in the scene + for (int rootNodeIndex : scene.nodes) { + traverseNode(rootNodeIndex, glm::mat4(1.0f)); + } + } + } + + std::map geometryMaterialMeshMap; // Map from geometry+material hash to unique MaterialMesh + + // Helper function to create a geometry hash for deduplication + auto createGeometryHash = [](const tinygltf::Primitive& primitive, int materialIndex) -> std::string { + std::string hash = "mat_" + std::to_string(materialIndex); + + // Add primitive attribute hashes to ensure unique geometry identification + if (primitive.indices >= 0) { + hash += "_idx_" + std::to_string(primitive.indices); + } + + for (const auto& attr : primitive.attributes) { + hash += "_" + attr.first + "_" + std::to_string(attr.second); + } + + return hash; + }; + + // Process all meshes with improved instancing support + for (size_t meshIndex = 0; meshIndex < gltfModel.meshes.size(); ++meshIndex) { + const auto& mesh = gltfModel.meshes[meshIndex]; - // Process all meshes and group by material - for (const auto& mesh : gltfModel.meshes) { - std::cout << "Processing mesh: " << mesh.name << std::endl; + // Check if this mesh has instances + auto instanceIt = meshInstanceTransforms.find(static_cast(meshIndex)); + std::vector instances; + + if (instanceIt == meshInstanceTransforms.end() || instanceIt->second.empty()) { + instances.emplace_back(1.0f); // Identity transform at origin + } else { + instances = instanceIt->second; + } + // Process each primitive (material group) in this mesh for (const auto& primitive : mesh.primitives) { // Get the material index for this primitive int materialIndex = primitive.material; @@ -501,23 +794,35 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { materialIndex = -1; // Use -1 for primitives without materials } - // Initialize vectors for this material if not already done - if (!materialVertices.contains(materialIndex)) { - materialVertices[materialIndex] = std::vector(); - materialIndices[materialIndex] = std::vector(); + // Create a unique geometry hash for this primitive and material combination + std::string geometryHash = createGeometryHash(primitive, materialIndex); + + // Check if we already have this exact geometry and material combination + if (!geometryMaterialMeshMap.contains(geometryHash)) { + // Create a new MaterialMesh for this unique geometry and material combination + MaterialMesh materialMesh; + materialMesh.materialIndex = materialIndex; - // Store material name for debugging + // Set material name if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { const auto& gltfMaterial = gltfModel.materials[materialIndex]; - materialNames[materialIndex] = gltfMaterial.name.empty() ? + materialMesh.materialName = gltfMaterial.name.empty() ? ("material_" + std::to_string(materialIndex)) : gltfMaterial.name; } else { - materialNames[materialIndex] = "no_material"; + materialMesh.materialName = "no_material"; } - std::cout << " Found material " << materialIndex << ": " << materialNames[materialIndex] << std::endl; + geometryMaterialMeshMap[geometryHash] = materialMesh; } - // Get indices for this material + + MaterialMesh& materialMesh = geometryMaterialMeshMap[geometryHash]; + + // Only process geometry if this MaterialMesh is empty (first time processing this geometry) + if (materialMesh.vertices.empty()) { + + auto vertexOffsetInMaterialMesh = static_cast(materialMesh.vertices.size()); + + // Get indices for this primitive if (primitive.indices >= 0) { const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; @@ -525,18 +830,18 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; - size_t indexOffset = materialVertices[materialIndex].size(); - - // Handle different index types + // Handle different index types with proper vertex offset adjustment if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { const auto* buf = static_cast(indexData); for (size_t i = 0; i < indexAccessor.count; ++i) { - materialIndices[materialIndex].push_back(buf[i] + indexOffset); + // FIXED: Add vertex offset to prevent index sharing between primitives + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); } } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { const auto* buf = static_cast(indexData); for (size_t i = 0; i < indexAccessor.count; ++i) { - materialIndices[materialIndex].push_back(buf[i] + indexOffset); + // FIXED: Add vertex offset to prevent index sharing between primitives + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); } } } @@ -577,18 +882,18 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { &normalBuffer.data[normalBufferView.byteOffset + normalAccessor.byteOffset]); } - // Create vertices for this material + // Create vertices in their original coordinate system (no transformation applied here) for (size_t i = 0; i < posAccessor.count; ++i) { Vertex vertex{}; - // Position + // Position (keep in an original coordinate system) vertex.position = glm::vec3( positions[i * 3 + 0], positions[i * 3 + 1], positions[i * 3 + 2] ); - // Normal (use extracted normals if available, otherwise default up) + // Normal (keep in an original coordinate system) if (normals) { vertex.normal = glm::vec3( normals[i * 3 + 0], @@ -612,32 +917,30 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Tangent (default right tangent for now, could be extracted from GLTF if available) vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - materialVertices[materialIndex].push_back(vertex); + materialMesh.vertices.push_back(vertex); + } + } // End of isFirstTimeProcessing block + + // Add all instances to this MaterialMesh (both new and existing geometry) + for (const glm::mat4& instanceTransform : instances) { + materialMesh.AddInstance(instanceTransform, static_cast(materialIndex)); } } } - // Store material meshes and combine for backward compatibility + // Convert geometry-based material mesh map to vector std::vector modelMaterialMeshes; + for (auto& val : geometryMaterialMeshMap | std::views::values) { + modelMaterialMeshes.push_back(val); + } + + // Process texture loading for each MaterialMesh std::vector combinedVertices; std::vector combinedIndices; - std::cout << "Processing " << materialVertices.size() << " materials:" << std::endl; - - for (const auto& materialPair : materialVertices) { - int materialIndex = materialPair.first; - const auto& vertices = materialPair.second; - const auto& indices = materialIndices[materialIndex]; - - std::cout << " Material " << materialIndex << " (" << materialNames[materialIndex] - << "): " << vertices.size() << " vertices, " << indices.size() << " indices" << std::endl; - - // Create MaterialMesh for this material - MaterialMesh materialMesh; - materialMesh.materialIndex = materialIndex; - materialMesh.materialName = materialNames[materialIndex]; - materialMesh.vertices = vertices; - materialMesh.indices = indices; + // Process texture loading for each MaterialMesh + for (auto & materialMesh : modelMaterialMeshes) { + int materialIndex = materialMesh.materialIndex; // Get ALL texture paths for this material (same as ParseGLTFDataOnly) if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { @@ -648,58 +951,90 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; if (texIndex < gltfModel.textures.size()) { const auto& texture = gltfModel.textures[texIndex]; + int imageIndex = -1; if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); + imageIndex = texture.source; + } else { + auto extIt = texture.extensions.find("KHR_texture_basisu"); + if (extIt != texture.extensions.end()) { + const tinygltf::Value& ext = extIt->second; + if (ext.Has("source") && ext.Get("source").IsInt()) { + int src = ext.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) { + imageIndex = src; + } + } + } + } + if (imageIndex >= 0) { + std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); materialMesh.baseColorTexturePath = textureId; - materialMesh.texturePath = textureId; // Keep for backward compatibility + materialMesh.texturePath = textureId; // Keep for backward compatibility (now baseColor‑tagged) - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; + // Load texture data (embedded or external) with caching + const auto& image = gltfModel.images[imageIndex]; if (!image.image.empty()) { - // Load embedded texture data - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Loaded embedded baseColor texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load embedded baseColor texture: " << textureId << std::endl; - } - } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the MaterialMesh to use the external texture path instead of gltf_texture_X - materialMesh.baseColorTexturePath = texturePath; - materialMesh.texturePath = texturePath; // Keep for backward compatibility - std::cout << " Loaded external baseColor texture: " << texturePath << std::endl; + if (!loadedTextures.contains(textureId)) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) { + loadedTextures.insert(textureId); + std::cout << " Loaded baseColor texture from memory: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } else { + std::cerr << " Failed to load baseColor texture from memory: " << textureId << std::endl; + } } else { - std::cerr << " Failed to load external baseColor texture: " << texturePath << std::endl; + std::cout << " Using cached baseColor texture: " << textureId << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for baseColor texture index " << texIndex << std::endl; } } } } else { // Since texture indices are -1, try to find external texture files by material name - std::string materialName = materialNames[materialIndex]; + std::string materialName = materialMesh.materialName; - // Look for external texture files that match this specific material + // Look for external texture files that match this specific material (case-insensitive) for (const auto & image : gltfModel.images) { if (!image.uri.empty()) { std::string imageUri = image.uri; + // Lowercase copies for robust matching + std::string imageUriLower = imageUri; + std::ranges::transform(imageUriLower, imageUriLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + std::string materialNameLower = materialName; + std::ranges::transform(materialNameLower, materialNameLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); // Check if this image belongs to this specific material based on naming patterns - // Look for BaseColor/Albedo textures that match the material name - if ((imageUri.find("BaseColor") != std::string::npos || - imageUri.find("Albedo") != std::string::npos || - imageUri.find("Diffuse") != std::string::npos) && - (imageUri.find(materialName) != std::string::npos || - materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { + // Look for basecolor/albedo/diffuse textures that match the material name + if ((imageUriLower.find("basecolor") != std::string::npos || + imageUriLower.find("albedo") != std::string::npos || + imageUriLower.find("diffuse") != std::string::npos) && + (imageUriLower.find(materialNameLower) != std::string::npos || + materialNameLower.find(imageUriLower.substr(0, imageUriLower.find('_'))) != std::string::npos)) { // Use the relative path from the GLTF directory - std::string texturePath = baseTexturePath + imageUri; - materialMesh.baseColorTexturePath = texturePath; - materialMesh.texturePath = texturePath; // Keep for backward compatibility - std::cout << " Found external baseColor texture for " << materialName << ": " << texturePath << std::endl; + std::string textureId = baseTexturePath + imageUri; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; + std::cout << " Loaded baseColor texture from memory (heuristic): " << textureId << std::endl; + } else { + std::cerr << " Failed to load heuristic baseColor texture from memory: " << textureId << std::endl; + } + } else { + // Fallback: load KTX2 from the file path and upload into memory + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(textureId, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; + std::cout << " Loaded baseColor KTX2 from file (heuristic): " << textureId << std::endl; + } else { + std::cerr << " Warning: Heuristic baseColor image has no decoded bytes and KTX2 fallback failed: " << imageUri << std::endl; + } + } break; } } @@ -726,31 +1061,31 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } else { std::cerr << " Failed to load embedded normal texture: " << textureId << std::endl; } - } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the MaterialMesh to use the external texture path instead of gltf_texture_X - materialMesh.normalTexturePath = texturePath; - std::cout << " Loaded external normal texture: " << texturePath << std::endl; - } else { - std::cerr << " Failed to load external normal texture: " << texturePath << std::endl; - } + } else { + std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; } } } } else { - // Look for external normal texture files that match this specific material - std::string materialName = materialNames[materialIndex]; + // Heuristic: search images for a normal texture for this material and load from memory + std::string materialName = materialMesh.materialName; for (const auto & image : gltfModel.images) { if (!image.uri.empty()) { std::string imageUri = image.uri; if (imageUri.find("Normal") != std::string::npos && (imageUri.find(materialName) != std::string::npos || materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { - std::string texturePath = baseTexturePath + imageUri; - materialMesh.normalTexturePath = texturePath; - std::cout << " Found external normal texture for " << materialName << ": " << texturePath << std::endl; + std::string textureId = baseTexturePath + imageUri; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { + materialMesh.normalTexturePath = textureId; + std::cout << " Loaded normal texture from memory (heuristic): " << textureId << std::endl; + } else { + std::cerr << " Failed to load heuristic normal texture from memory: " << textureId << std::endl; + } + } else { + std::cerr << " Warning: Heuristic normal image has no decoded bytes: " << imageUri << std::endl; + } break; } } @@ -769,30 +1104,22 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded embedded metallic-roughness texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; + materialMesh.metallicRoughnessTexturePath = textureId; + std::cout << " Loaded metallic-roughness texture from memory: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else { - std::cerr << " Failed to load embedded metallic-roughness texture: " << textureId << std::endl; - } - } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the MaterialMesh to use the external texture path instead of gltf_texture_X - materialMesh.metallicRoughnessTexturePath = texturePath; - std::cout << " Loaded external metallic-roughness texture: " << texturePath << std::endl; - } else { - std::cerr << " Failed to load external metallic-roughness texture: " << texturePath << std::endl; + std::cerr << " Failed to load metallic-roughness texture from memory: " << textureId << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; } } } } else { // Look for external metallic-roughness texture files that match this specific material - std::string materialName = materialNames[materialIndex]; + std::string materialName = materialMesh.materialName; for (const auto & image : gltfModel.images) { if (!image.uri.empty()) { std::string imageUri = image.uri; @@ -822,30 +1149,22 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { // Load texture data (embedded or external) const auto& image = gltfModel.images[texture.source]; if (!image.image.empty()) { - // Load embedded texture data if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { - std::cout << " Loaded embedded occlusion texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; + materialMesh.occlusionTexturePath = textureId; + std::cout << " Loaded occlusion texture from memory: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; } else { - std::cerr << " Failed to load embedded occlusion texture: " << textureId << std::endl; - } - } else if (!image.uri.empty()) { - // Load external texture file - std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the MaterialMesh to use the external texture path instead of gltf_texture_X - materialMesh.occlusionTexturePath = texturePath; - std::cout << " Loaded external occlusion texture: " << texturePath << std::endl; - } else { - std::cerr << " Failed to load external occlusion texture: " << texturePath << std::endl; + std::cerr << " Failed to load occlusion texture from memory: " << textureId << std::endl; } + } else { + std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; } } } } else { - // Look for external occlusion texture files that match this specific material - std::string materialName = materialNames[materialIndex]; + // Heuristic: search images for an occlusion texture for this material and load from memory + std::string materialName = materialMesh.materialName; for (const auto & image : gltfModel.images) { if (!image.uri.empty()) { std::string imageUri = image.uri; @@ -853,9 +1172,17 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { imageUri.find("AO") != std::string::npos) && (imageUri.find(materialName) != std::string::npos || materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { - std::string texturePath = baseTexturePath + imageUri; - materialMesh.occlusionTexturePath = texturePath; - std::cout << " Found external occlusion texture for " << materialName << ": " << texturePath << std::endl; + std::string textureId = baseTexturePath + imageUri; + if (!image.image.empty()) { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), image.width, image.height, image.component)) { + materialMesh.occlusionTexturePath = textureId; + std::cout << " Loaded occlusion texture from memory (heuristic): " << textureId << std::endl; + } else { + std::cerr << " Failed to load heuristic occlusion texture from memory: " << textureId << std::endl; + } + } else { + std::cerr << " Warning: Heuristic occlusion image has no decoded bytes: " << imageUri << std::endl; + } break; } } @@ -883,21 +1210,16 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cerr << " Failed to load embedded emissive texture: " << textureId << std::endl; } } else if (!image.uri.empty()) { - // Load external texture file + // Record external texture file path (loaded later by renderer) std::string texturePath = baseTexturePath + image.uri; - if (renderer->LoadTexture(texturePath)) { - // Update the MaterialMesh to use the external texture path instead of gltf_texture_X - materialMesh.emissiveTexturePath = texturePath; - std::cout << " Loaded external emissive texture: " << texturePath << std::endl; - } else { - std::cerr << " Failed to load external emissive texture: " << texturePath << std::endl; - } + materialMesh.emissiveTexturePath = texturePath; + std::cout << " External emissive texture path: " << texturePath << std::endl; } } } } else { // Look for external emissive texture files that match this specific material - std::string materialName = materialNames[materialIndex]; + std::string materialName = materialMesh.materialName; for (const auto & image : gltfModel.images) { if (!image.uri.empty()) { std::string imageUri = image.uri; @@ -915,14 +1237,20 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } } - modelMaterialMeshes.push_back(materialMesh); + // Add to combined mesh for backward compatibility (keep vertices in an original coordinate system) + if (!materialMesh.instances.empty()) { + size_t vertexOffset = combinedVertices.size(); - // Also add to combined mesh for backward compatibility - size_t vertexOffset = combinedVertices.size(); - combinedVertices.insert(combinedVertices.end(), vertices.begin(), vertices.end()); + // FIXED: Don't transform vertices - keep them in the original coordinate system + // Instance transforms should be handled by the instancing system, not applied to vertex data + for (const auto& vertex : materialMesh.vertices) { + // Use vertices as-is without any transformation + combinedVertices.push_back(vertex); + } - for (uint32_t index : indices) { - combinedIndices.push_back(index + vertexOffset); + for (uint32_t index : materialMesh.indices) { + combinedIndices.push_back(index + vertexOffset); + } } } @@ -945,142 +1273,6 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { return true; } -bool ModelLoader::LoadPBRTextures(Material* material, - const std::string& albedoMap, - const std::string& normalMap, - const std::string& metallicRoughnessMap, - const std::string& aoMap, - const std::string& emissiveMap) const { - if (!material) { - std::cerr << "ModelLoader::LoadPBRTextures: Material is null" << std::endl; - return false; - } - - if (!renderer) { - std::cerr << "ModelLoader::LoadPBRTextures: Renderer is null" << std::endl; - return false; - } - - std::cout << "Loading PBR textures for material: " << material->GetName() << std::endl; - - bool success = true; - - // Load albedo map or create default - if (!albedoMap.empty()) { - std::cout << " Loading albedo map: " << albedoMap << std::endl; - material->albedoTexturePath = albedoMap; - if (!renderer->LoadTexture(albedoMap)) { - std::cerr << " Failed to load albedo texture: " << albedoMap << std::endl; - success = false; - } - } else { - // Use shared default albedo texture (much more efficient than creating per-material textures) - std::cout << " Using shared default albedo texture" << std::endl; - material->albedoTexturePath = Renderer::SHARED_DEFAULT_ALBEDO_ID; - } - - // Load normal map or create default - if (!normalMap.empty()) { - std::cout << " Loading normal map: " << normalMap << std::endl; - material->normalTexturePath = normalMap; - if (!renderer->LoadTexture(normalMap)) { - std::cerr << " Failed to load normal texture: " << normalMap << std::endl; - success = false; - } - } else { - // Use shared default normal texture (much more efficient than creating per-material textures) - std::cout << " Using shared default normal texture" << std::endl; - material->normalTexturePath = Renderer::SHARED_DEFAULT_NORMAL_ID; - } - - // Load metallic-roughness map or create default - if (!metallicRoughnessMap.empty()) { - std::cout << " Loading metallic-roughness map: " << metallicRoughnessMap << std::endl; - material->metallicRoughnessTexturePath = metallicRoughnessMap; - if (!renderer->LoadTexture(metallicRoughnessMap)) { - std::cerr << " Failed to load metallic-roughness texture: " << metallicRoughnessMap << std::endl; - success = false; - } - } else { - // Use shared default metallic-roughness texture (much more efficient than creating per-material textures) - std::cout << " Using shared default metallic-roughness texture" << std::endl; - material->metallicRoughnessTexturePath = Renderer::SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; - } - - // Load ambient occlusion map or create default - if (!aoMap.empty()) { - std::cout << " Loading ambient occlusion map: " << aoMap << std::endl; - material->occlusionTexturePath = aoMap; - if (!renderer->LoadTexture(aoMap)) { - std::cerr << " Failed to load occlusion texture: " << aoMap << std::endl; - success = false; - } - } else { - // Use shared default occlusion texture (much more efficient than creating per-material textures) - std::cout << " Using shared default occlusion texture" << std::endl; - material->occlusionTexturePath = Renderer::SHARED_DEFAULT_OCCLUSION_ID; - } - - // Load emissive map or create default - if (!emissiveMap.empty()) { - std::cout << " Loading emissive map: " << emissiveMap << std::endl; - material->emissiveTexturePath = emissiveMap; - if (!renderer->LoadTexture(emissiveMap)) { - std::cerr << " Failed to load emissive texture: " << emissiveMap << std::endl; - success = false; - } - } else { - // Use shared default emissive texture (much more efficient than creating per-material textures) - std::cout << " Using shared default emissive texture" << std::endl; - material->emissiveTexturePath = Renderer::SHARED_DEFAULT_EMISSIVE_ID; - } - - std::cout << "PBR texture paths stored for material: " << material->GetName() << std::endl; - return success; -} - -std::string ModelLoader::GetFirstMaterialTexturePath(const std::string& modelName) { - // Get material meshes for this specific model - auto it = materialMeshes.find(modelName); - if (it == materialMeshes.end()) { - std::cout << "No material meshes found for model: " << modelName << std::endl; - return ""; - } - - const auto& modelMaterialMeshes = it->second; - - // First, try to find a material mesh with a texture path (prioritizing base color) - for (const auto& materialMesh : modelMaterialMeshes) { - if (!materialMesh.texturePath.empty()) { - std::cout << "Found texture path for model " << modelName << ": " << materialMesh.texturePath << std::endl; - return materialMesh.texturePath; - } - } - - // If no texture path found in MaterialMesh, try to get from the actual materials - // Only look for albedo textures to ensure non-PBR rendering doesn't use normal/metallic maps - for (const auto& materialMesh : modelMaterialMeshes) { - const std::string& materialName = materialMesh.materialName; - if (materialName.empty()) continue; - - auto materialIt = materials.find(materialName); - if (materialIt != materials.end()) { - const auto& material = materialIt->second; - - // Only return albedo texture for non-PBR rendering compatibility - if (!material->albedoTexturePath.empty()) { - std::cout << "Found albedo texture path for model " << modelName << ": " << material->albedoTexturePath << std::endl; - return material->albedoTexturePath; - } - // Don't fall back to normal or metallic-roughness textures to avoid - // using them in non-PBR rendering where they would be incorrect - } - } - - std::cout << "No texture path found for model: " << modelName << std::endl; - return ""; -} - std::vector ModelLoader::GetExtractedLights(const std::string& modelName) const { std::vector lights; @@ -1102,7 +1294,7 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m // Check if this material has emissive properties (no threshold filtering) float emissiveIntensity = glm::length(material->emissive) * material->emissiveStrength; - if (emissiveIntensity >= 0.0f) { // Accept all emissive materials, including zero intensity + if (emissiveIntensity >= 0.0f) { // Accept all emissive materials, including zero intensities // Calculate the center position of the emissive surface glm::vec3 center(0.0f); if (!materialMesh.vertices.empty()) { @@ -1131,7 +1323,7 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m // Create an emissive light source ExtractedLight emissiveLight; emissiveLight.type = ExtractedLight::Type::Emissive; - emissiveLight.position = lightPosition; // Use offset position + emissiveLight.position = lightPosition; // Use the offset position emissiveLight.color = material->emissive; emissiveLight.intensity = material->emissiveStrength; emissiveLight.range = 10.0f; // Default range for emissive lights @@ -1219,15 +1411,15 @@ bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const // Parse light intensity if (lightValue.Has("intensity") && lightValue.Get("intensity").IsNumber()) { - light.intensity = static_cast(lightValue.Get("intensity").Get()); + light.intensity = static_cast(lightValue.Get("intensity").Get()) * LIGHT_SCALE_FACTOR; } - // Parse light range (for point and spot lights) + // Parse light range (for point and spotlights) if (lightValue.Has("range") && lightValue.Get("range").IsNumber()) { light.range = static_cast(lightValue.Get("range").Get()); } - // Parse spot light specific parameters + // Parse spotlights specific parameters if (light.type == ExtractedLight::Type::Spot && lightValue.Has("spot")) { const tinygltf::Value& spotValue = lightValue.Get("spot"); if (spotValue.Has("innerConeAngle") && spotValue.Get("innerConeAngle").IsNumber()) { @@ -1263,11 +1455,11 @@ bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const ); } - // Extract direction from node rotation (for directional and spot lights) + // Extract direction from node rotation (for directional and spotlights) if (node.rotation.size() >= 4 && (lights[lightIndex].type == ExtractedLight::Type::Directional || lights[lightIndex].type == ExtractedLight::Type::Spot)) { - // Convert quaternion to direction vector + // Convert quaternion to a direction vector glm::quat rotation( static_cast(node.rotation[3]), // w static_cast(node.rotation[0]), // x @@ -1294,101 +1486,3 @@ bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const return lights.empty(); } -bool ModelLoader::LoadEmbeddedGLTFTextures(const std::string& filename) const { - std::cout << "Loading embedded GLTF textures from: " << filename << std::endl; - - if (!renderer) { - std::cerr << "LoadEmbeddedGLTFTextures: Renderer is null" << std::endl; - return false; - } - - // Create a tinygltf loader with proper image loading - tinygltf::Model gltfModel; - tinygltf::TinyGLTF loader; - std::string err; - std::string warn; - - // Set up a proper image loader callback using stb_image (same as ParseGLTF) - loader.SetImageLoader([](tinygltf::Image* image, const int image_idx, std::string* err, - std::string* warn, int req_width, int req_height, - const unsigned char* bytes, int size, void* user_data) -> bool { - // Use stb_image to decode the image data - int width, height, channels; - unsigned char* data = stbi_load_from_memory(bytes, size, &width, &height, &channels, 0); - - if (!data) { - if (err) { - *err = "Failed to load image with stb_image: " + std::string(stbi_failure_reason()); - } - return false; - } - - // Set image properties - image->width = width; - image->height = height; - image->component = channels; - image->bits = 8; - image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; - - // Copy image data - size_t image_size = width * height * channels; - image->image.resize(image_size); - std::memcpy(image->image.data(), data, image_size); - - // Free stb_image data - stbi_image_free(data); - - std::cout << " Loaded embedded texture: " << width << "x" << height << " with " << channels << " channels" << std::endl; - return true; - }, nullptr); - - // Load the GLTF file - bool ret = false; - if (filename.find(".glb") != std::string::npos) { - ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, filename); - } else { - ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, filename); - } - - if (!warn.empty()) { - std::cout << "GLTF Warning: " << warn << std::endl; - } - - if (!err.empty()) { - std::cerr << "GLTF Error: " << err << std::endl; - return false; - } - - if (!ret) { - std::cerr << "Failed to parse GLTF file for texture loading: " << filename << std::endl; - return false; - } - - std::cout << "Successfully loaded GLTF file for texture extraction" << std::endl; - - // Load all embedded textures using LoadTextureFromMemory - int texturesLoaded = 0; - for (size_t texIndex = 0; texIndex < gltfModel.textures.size(); ++texIndex) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - const auto& image = gltfModel.images[texture.source]; - - if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - std::cout << " Loaded embedded texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - texturesLoaded++; - } else { - std::cerr << " Failed to load embedded texture: " << textureId << std::endl; - } - } else { - std::cerr << " Empty image data for texture: " << textureId << std::endl; - } - } - } - - std::cout << "Successfully loaded " << texturesLoaded << " embedded GLTF textures" << std::endl; - return texturesLoaded > 0; -} diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index ce3413ff..2fbe6eea 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -32,6 +32,7 @@ class Material { float ao = 1.0f; glm::vec3 emissive = glm::vec3(0.0f); float emissiveStrength = 1.0f; // KHR_materials_emissive_strength extension + float alpha = 1.0f; // Base color alpha (from MR baseColorFactor or SpecGloss diffuseFactor) // Texture paths for PBR materials std::string albedoTexturePath; @@ -58,12 +59,12 @@ struct ExtractedLight { Type type = Type::Point; glm::vec3 position = glm::vec3(0.0f); - glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // For directional/spot lights + glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // For directional/spotlights glm::vec3 color = glm::vec3(1.0f); float intensity = 1.0f; - float range = 100.0f; // For point/spot lights - float innerConeAngle = 0.0f; // For spot lights - float outerConeAngle = 0.785398f; // For spot lights (45 degrees) + float range = 100.0f; // For point/spotlights + float innerConeAngle = 0.0f; // For spotlights + float outerConeAngle = 0.785398f; // For spotlights (45 degrees) std::string sourceMaterial; // Name of source material (for emissive lights) }; @@ -106,6 +107,37 @@ struct MaterialMesh { std::string metallicRoughnessTexturePath; // Metallic-roughness texture std::string occlusionTexturePath; // Ambient occlusion texture std::string emissiveTexturePath; // Emissive texture + + // Instancing support + std::vector instances; // Instance data for instanced rendering + bool isInstanced = false; // Flag to indicate if this mesh uses instancing + + /** + * @brief Add an instance with the given transform matrix. + * @param transform The transform matrix for this instance. + * @param matIndex The material index for this instance (default: use materialIndex). + */ + void AddInstance(const glm::mat4& transform, uint32_t matIndex = 0) { + if (matIndex == 0) matIndex = static_cast(materialIndex); + instances.emplace_back(transform, matIndex); + isInstanced = instances.size() > 1; + } + + /** + * @brief Get the number of instances. + * @return Number of instances (0 if not instanced, >= 1 if instanced). + */ + [[nodiscard]] size_t GetInstanceCount() const { + return instances.size(); + } + + /** + * @brief Check if this mesh uses instancing. + * @return True if instanced (more than 1 instance), false otherwise. + */ + [[nodiscard]] bool IsInstanced() const { + return isInstanced; + } }; /** @@ -157,10 +189,10 @@ class ModelLoader { /** * @brief Initialize the model loader. - * @param renderer Pointer to the renderer. + * @param _renderer Pointer to the renderer. * @return True if initialization was successful, false otherwise. */ - bool Initialize(Renderer* renderer); + bool Initialize(Renderer* _renderer); /** * @brief Load a model from a GLTF file. @@ -169,22 +201,6 @@ class ModelLoader { */ Model* LoadGLTF(const std::string& filename); - /** - * @brief Load a model from a GLTF file with PBR materials. - * @param filename The path to the GLTF file. - * @param albedoMap The path to the albedo texture. - * @param normalMap The path to the normal texture. - * @param metallicRoughnessMap The path to the metallic-roughness texture. - * @param aoMap The path to the ambient occlusion texture. - * @param emissiveMap The path to the emissive texture. - * @return Pointer to the loaded model, or nullptr if loading failed. - */ - Model* LoadGLTFWithPBR(const std::string& filename, - const std::string& albedoMap, - const std::string& normalMap, - const std::string& metallicRoughnessMap, - const std::string& aoMap, - const std::string& emissiveMap); /** * @brief Get a model by name. @@ -193,29 +209,7 @@ class ModelLoader { */ Model* GetModel(const std::string& name); - /** - * @brief Create a new material with PBR properties. - * @param name The name of the material. - * @param albedo The albedo color. - * @param metallic The metallic factor. - * @param roughness The roughness factor. - * @param ao The ambient occlusion factor. - * @param emissive The emissive color. - * @return Pointer to the created material, or nullptr if creation failed. - */ - Material* CreatePBRMaterial(const std::string& name, - const glm::vec3& albedo, - float metallic, - float roughness, - float ao, - const glm::vec3& emissive); - /** - * @brief Get the first available material texture path for a model. - * @param modelName The name of the model. - * @return The texture path of the first material, or empty string if none found. - */ - std::string GetFirstMaterialTexturePath(const std::string& modelName); /** * @brief Get extracted lights from a loaded model. @@ -238,12 +232,6 @@ class ModelLoader { */ Material* GetMaterial(const std::string& materialName) const; - /** - * @brief Load embedded GLTF textures. - * @param filename The path to the GLTF file. - * @return True if textures were loaded successfully, false otherwise. - */ - bool LoadEmbeddedGLTFTextures(const std::string& filename) const; private: // Reference to the renderer @@ -261,6 +249,8 @@ class ModelLoader { // Material meshes per model std::unordered_map> materialMeshes; + float light_scale = 1.0f; + /** * @brief Parse a GLTF file. * @param filename The path to the GLTF file. @@ -279,12 +269,6 @@ class ModelLoader { * @param emissiveMap The path to the emissive texture. * @return True if loading was successful, false otherwise. */ - bool LoadPBRTextures(Material* material, - const std::string& albedoMap, - const std::string& normalMap, - const std::string& metallicRoughnessMap, - const std::string& aoMap, - const std::string& emissiveMap) const; /** * @brief Extract lights from GLTF punctual lights extension. diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index 5d1e5dac..1e19f2e8 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -2,6 +2,7 @@ #include "entity.h" #include "renderer.h" #include "transform_component.h" +#include "mesh_component.h" #include #include @@ -17,11 +18,10 @@ class ConcreteRigidBody : public RigidBody { public: ConcreteRigidBody(Entity* entity, CollisionShape shape, float mass) : entity(entity), shape(shape), mass(mass) { - // Initialize with entity's transform if available + // Initialize with the entity's transform if available if (entity) { // Get the position, rotation, and scale from the entity's transform component - auto* transform = entity->GetComponent(); - if (transform) { + if (auto* transform = entity->GetComponent()) { position = transform->GetPosition(); rotation = glm::quat(transform->GetRotation()); // Convert from Euler angles to quaternion scale = transform->GetScale(); @@ -36,46 +36,44 @@ class ConcreteRigidBody : public RigidBody { ~ConcreteRigidBody() override = default; - void SetPosition(const glm::vec3& position) override { - this->position = position; + void SetPosition(const glm::vec3& _position) override { + position = _position; // Update entity transform component for visual representation if (entity) { - auto* transform = entity->GetComponent(); - if (transform) { - transform->SetPosition(position); + if (auto* transform = entity->GetComponent()) { + transform->SetPosition(_position); } } } - void SetRotation(const glm::quat& rotation) override { - this->rotation = rotation; + void SetRotation(const glm::quat& _rotation) override { + rotation = _rotation; // Update entity transform component for visual representation if (entity) { - auto* transform = entity->GetComponent(); - if (transform) { + if (auto* transform = entity->GetComponent()) { // Convert quaternion to Euler angles for the transform component - glm::vec3 eulerAngles = glm::eulerAngles(rotation); + glm::vec3 eulerAngles = glm::eulerAngles(_rotation); transform->SetRotation(eulerAngles); } } } - void SetScale(const glm::vec3& scale) override { - this->scale = scale; + void SetScale(const glm::vec3& _scale) override { + scale = _scale; } - void SetMass(float mass) override { - this->mass = mass; + void SetMass(float _mass) override { + mass = _mass; } - void SetRestitution(float restitution) override { - this->restitution = restitution; + void SetRestitution(float _restitution) override { + restitution = _restitution; } - void SetFriction(float friction) override { - this->friction = friction; + void SetFriction(float _friction) override { + friction = _friction; } void ApplyForce(const glm::vec3& force, const glm::vec3& localPosition) override { @@ -112,13 +110,13 @@ class ConcreteRigidBody : public RigidBody { return angularVelocity; } - void SetKinematic(bool kinematic) override { + void SetKinematic(bool _kinematic) override { // Prevent balls from being set as kinematic - they should always be dynamic - if (entity && entity->GetName().find("Ball_") == 0 && kinematic) { + if (entity && entity->GetName().find("Ball_") == 0 && _kinematic) { return; } - this->kinematic = kinematic; + kinematic = _kinematic; } [[nodiscard]] bool IsKinematic() const override { @@ -209,7 +207,7 @@ void PhysicsSystem::Update(float deltaTime) { } SimulatePhysicsOnGPU(deltaTime); } else { - // NO CPU FALLBACK - GPU physics must work or physics is disabled + // NO CPU FALLBACK - GPU physics must work, or physics is disabled static bool noFallbackLogged = false; if (!noFallbackLogged) { noFallbackLogged = true; @@ -250,8 +248,8 @@ bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { return false; } -void PhysicsSystem::SetGravity(const glm::vec3& gravity) { - this->gravity = gravity; +void PhysicsSystem::SetGravity(const glm::vec3& _gravity) { + gravity = _gravity; } glm::vec3 PhysicsSystem::GetGravity() const { @@ -330,7 +328,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, for (int i = 0; i < 3; i++) { if (std::abs(normalizedDirection[i]) < 0.0001f) { - // Ray is parallel to slab, check if origin is within slab + // Ray is parallel to the slab, check if origin is within slab if (origin[i] < boxMin[i] || origin[i] > boxMax[i]) { // No intersection hit = false; @@ -421,8 +419,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, } case CollisionShape::Mesh: { // Proper mesh intersection test using triangle data - auto* meshComponent = entity->GetComponent(); - if (meshComponent) { + if (auto* meshComponent = entity->GetComponent()) { const auto& vertices = meshComponent->GetVertices(); const auto& indices = meshComponent->GetIndices(); @@ -436,8 +433,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, glm::vec3 v2 = vertices[indices[i + 2]].position; // Transform vertices to world space - auto* transform = entity->GetComponent(); - if (transform) { + if (auto* transform = entity->GetComponent()) { glm::mat4 transformMatrix = transform->GetModelMatrix(); v0 = glm::vec3(transformMatrix * glm::vec4(v0, 1.0f)); v1 = glm::vec3(transformMatrix * glm::vec4(v1, 1.0f)); @@ -531,11 +527,7 @@ static vk::raii::ShaderModule createShaderModule(const vk::raii::Device& device, createInfo.codeSize = code.size(); createInfo.pCode = reinterpret_cast(code.data()); - try { - return {device, createInfo}; - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create shader module: " + std::string(e.what())); - } + return {device, createInfo}; } bool PhysicsSystem::InitializeVulkanResources() { @@ -664,7 +656,7 @@ bool PhysicsSystem::InitializeVulkanResources() { vk::DeviceSize collisionBufferSize = sizeof(GPUCollisionData) * maxGPUCollisions; vk::DeviceSize pairBufferSize = sizeof(uint32_t) * 2 * maxGPUCollisions; vk::DeviceSize counterBufferSize = sizeof(uint32_t) * 2; - vk::DeviceSize paramsBufferSize = sizeof(PhysicsParams); + vk::DeviceSize paramsBufferSize = ((sizeof(PhysicsParams) + 63) / 64) * 64; // Create a physics buffer vk::BufferCreateInfo bufferInfo; @@ -730,7 +722,7 @@ bool PhysicsSystem::InitializeVulkanResources() { throw std::runtime_error("Failed to create pair buffer: " + std::string(e.what())); } - // Create counter-buffer + // Create the counter-buffer bufferInfo.size = counterBufferSize; try { vulkanResources.counterBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); @@ -839,7 +831,7 @@ bool PhysicsSystem::InitializeVulkanResources() { vk::DescriptorBufferInfo paramsBufferInfo; paramsBufferInfo.buffer = *vulkanResources.paramsBuffer; paramsBufferInfo.offset = 0; - paramsBufferInfo.range = VK_WHOLE_SIZE; // Use VK_WHOLE_SIZE to ensure entire buffer is accessible + paramsBufferInfo.range = VK_WHOLE_SIZE; // Use VK_WHOLE_SIZE to ensure the entire buffer is accessible std::array descriptorWrites; @@ -944,10 +936,10 @@ void PhysicsSystem::CleanupVulkanResources() { vulkanResources.broadPhaseShaderModule = nullptr; vulkanResources.integrateShaderModule = nullptr; - // 5. Destroy descriptor pool after descriptor sets are cleared + // 5. Destroy the descriptor pool after descriptor sets are cleared vulkanResources.descriptorPool = nullptr; - // 6. Destroy command buffer before command pool + // 6. Destroy the command buffer before the command pool vulkanResources.commandBuffer = nullptr; vulkanResources.commandPool = nullptr; @@ -1015,7 +1007,7 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { auto initialForce = glm::vec3(0.0f); auto initialTorque = glm::vec3(0.0f); - // For dynamic bodies (balls), allow forces to be applied by the shader + // For dynamic bodies (balls), allow forces to be applied by // The shader will add gravity and other forces each frame gpuData[i].force = glm::vec4(initialForce, concreteRigidBody->IsKinematic() ? 1.0f : 0.0f); gpuData[i].torque = glm::vec4(initialTorque, 1.0f); // Always use gravity @@ -1032,10 +1024,42 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { gpuData[i].colliderData2 = glm::vec4(0.0f); break; case CollisionShape::Mesh: - // Represent mesh as a large bounding box for GPU collision detection - // This provides basic collision detection while maintaining GPU performance - gpuData[i].colliderData = glm::vec4(5.0f, 5.0f, 5.0f, static_cast(2)); // 2 = Mesh (as Box) - gpuData[i].colliderData2 = glm::vec4(0.0f); + { + // Compute an axis-aligned bounding box from the entity's mesh in WORLD space + // and pass half-extents and local offset to the GPU. This enables sphere-geometry + // collisions against actual imported GLTF geometry rather than a constant box. + glm::vec3 halfExtents(5.0f); + glm::vec3 localOffset(0.0f); + + if (auto* entity = concreteRigidBody->GetEntity()) { + auto* meshComp = entity->GetComponent(); + auto* xform = entity->GetComponent(); + if (meshComp && xform && meshComp->HasLocalAABB()) { + glm::vec3 localMin = meshComp->GetLocalAABBMin(); + glm::vec3 localMax = meshComp->GetLocalAABBMax(); + glm::vec3 localCenter = 0.5f * (localMin + localMax); + glm::vec3 localHalfExtents = 0.5f * (localMax - localMin); + + glm::mat4 model = xform->GetModelMatrix(); + glm::vec3 centerWS = glm::vec3(model * glm::vec4(localCenter, 1.0f)); + + glm::mat3 RS = glm::mat3(model); + glm::mat3 absRS; + absRS[0] = glm::abs(RS[0]); + absRS[1] = glm::abs(RS[1]); + absRS[2] = glm::abs(RS[2]); + + glm::vec3 worldHalfExtents = absRS * localHalfExtents; + halfExtents = glm::max(worldHalfExtents, glm::vec3(0.01f)); + + // Offset relative to rigid body position + localOffset = centerWS - concreteRigidBody->GetPosition(); + } + } + + gpuData[i].colliderData = glm::vec4(halfExtents, static_cast(2)); // 2 = Mesh (as Box) + gpuData[i].colliderData2 = glm::vec4(localOffset, 0.0f); + } break; default: gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); // Invalid @@ -1062,13 +1086,13 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { // CRITICAL FIX: Explicit memory flush to ensure HOST_COHERENT memory is fully visible to GPU // Even with HOST_COHERENT flag, some systems may have cache coherency issues with partial writes - // This ensures the entire PhysicsParams struct is flushed before GPU operations begin + // Use VK_WHOLE_SIZE to avoid nonCoherentAtomSize alignment validation errors try { const vk::raii::Device& device = renderer->GetRaiiDevice(); vk::MappedMemoryRange flushRange; flushRange.memory = *vulkanResources.paramsBufferMemory; flushRange.offset = 0; - flushRange.size = sizeof(PhysicsParams); + flushRange.size = VK_WHOLE_SIZE; device.flushMappedMemoryRanges(flushRange); } catch (const std::exception& e) { @@ -1118,7 +1142,7 @@ void PhysicsSystem::ReadbackGPUPhysicsData() const { } } -void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { +void PhysicsSystem::SimulatePhysicsOnGPU(const float deltaTime) const { if (!renderer) { fprintf(stderr, "SimulatePhysicsOnGPU: No renderer available"); return; @@ -1188,8 +1212,10 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { // Step 2: Broad-phase collision detection vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.broadPhasePipeline); uint32_t numPairs = (rigidBodies.size() * (rigidBodies.size() - 1)) / 2; + // Dispatch number of workgroups matching [numthreads(64,1,1)] in BroadPhaseCS + // One workgroup has 64 threads, each processes one pair by index uint32_t broadPhaseThreads = (numPairs + 63) / 64; - vulkanResources.commandBuffer.dispatch(broadPhaseThreads, 1, 1); + vulkanResources.commandBuffer.dispatch(std::max(1u, broadPhaseThreads), 1, 1); // Memory barrier to ensure the broad phase is complete before the narrow phase vulkanResources.commandBuffer.pipelineBarrier( @@ -1230,7 +1256,7 @@ void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { const vk::raii::Device& device = renderer->GetRaiiDevice(); device.resetFences(*vulkanResources.computeFence); - // Submit command buffer with dedicated fence for synchronization + // Submit the command buffer with the dedicated fence for synchronization vk::CommandBuffer cmdBuffer = *vulkanResources.commandBuffer; renderer->SubmitToComputeQueue(cmdBuffer, *vulkanResources.computeFence); @@ -1249,4 +1275,4 @@ void PhysicsSystem::CleanupMarkedBodies() { ++it; } } -} \ No newline at end of file +} diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index b79d511a..1ecb86d2 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -169,10 +169,10 @@ struct PhysicsParams { }; /** - * @brief Structure to store collision prediction data for ray-based collision system. + * @brief Structure to store collision prediction data for a ray-based collision system. */ struct CollisionPrediction { - float collisionTime = -1.0f; // Time within deltaTime when collision occurs (-1 = no collision) + float collisionTime = -1.0f; // Time within deltaTime when the collision occurs (-1 = no collision) glm::vec3 collisionPoint; // World position where collision occurs glm::vec3 collisionNormal; // Surface normal at collision point glm::vec3 newVelocity; // Predicted velocity after bounce @@ -229,9 +229,9 @@ class PhysicsSystem { /** * @brief Set the gravity of the physics world. - * @param gravity The gravity vector. + * @param _gravity The gravity vector. */ - void SetGravity(const glm::vec3& gravity); + void SetGravity(const glm::vec3& _gravity); /** * @brief Get the gravity of the physics world. @@ -260,7 +260,7 @@ class PhysicsSystem { /** * @brief Check if GPU acceleration is enabled. - * @return True if GPU acceleration is enabled, false otherwise. + * @return True, if GPU acceleration is enabled, false otherwise. */ [[nodiscard]] bool IsGPUAccelerationEnabled() const { return gpuAccelerationEnabled; } @@ -271,16 +271,16 @@ class PhysicsSystem { void SetMaxGPUObjects(uint32_t maxObjects) { maxGPUObjects = maxObjects; } /** - * @brief Set the renderer to use for GPU acceleration. - * @param renderer The renderer. + * @brief Set the renderer to use during GPU acceleration. + * @param _renderer The renderer. */ - void SetRenderer(Renderer* renderer) { this->renderer = renderer; } + void SetRenderer(Renderer* _renderer) { renderer = _renderer; } /** * @brief Set the current camera position for geometry-relative ball checking. - * @param cameraPosition The current camera position. + * @param _cameraPosition The current camera position. */ - void SetCameraPosition(const glm::vec3& cameraPosition) { this->cameraPosition = cameraPosition; } + void SetCameraPosition(const glm::vec3& _cameraPosition) { cameraPosition = _cameraPosition; } private: /** @@ -363,5 +363,5 @@ class PhysicsSystem { void ReadbackGPUPhysicsData() const; // Perform GPU-accelerated physics simulation - void SimulatePhysicsOnGPU(float deltaTime); + void SimulatePhysicsOnGPU(float deltaTime) const; }; diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp index cea1cd9d..29302ecd 100644 --- a/attachments/simple_engine/platform.cpp +++ b/attachments/simple_engine/platform.cpp @@ -2,7 +2,7 @@ #include -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) // Android platform implementation AndroidPlatform::AndroidPlatform(android_app* androidApp) diff --git a/attachments/simple_engine/platform.h b/attachments/simple_engine/platform.h index 021aae31..39f7b145 100644 --- a/attachments/simple_engine/platform.h +++ b/attachments/simple_engine/platform.h @@ -4,7 +4,7 @@ #include #include -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) #include #include #include @@ -126,7 +126,7 @@ class Platform { virtual void SetWindowTitle(const std::string& title) = 0; }; -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) /** * @brief Android implementation of the Platform interface. */ @@ -451,7 +451,7 @@ class DesktopPlatform : public Platform { */ template std::unique_ptr CreatePlatform(Args&&... args) { -#if PLATFORM_ANDROID +#if defined(PLATFORM_ANDROID) return std::make_unique(std::forward(args)...); #else return std::make_unique(); diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index b979db45..5f5676fa 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -42,7 +42,7 @@ struct SwapChainSupportDetails { }; /** - * @brief Structure for individual light data in storage buffer. + * @brief Structure for individual light data in the storage buffer. */ struct LightData { alignas(16) glm::vec4 position; // Light position (w component unused) @@ -50,12 +50,12 @@ struct LightData { alignas(16) glm::mat4 lightSpaceMatrix; // Light space matrix for shadow mapping alignas(4) int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive alignas(4) float range; // Light range - alignas(4) float innerConeAngle; // For spot lights - alignas(4) float outerConeAngle; // For spot lights + alignas(4) float innerConeAngle; // For spotlights + alignas(4) float outerConeAngle; // For spotlights }; /** - * @brief Structure for uniform buffer object (now without fixed light arrays). + * @brief Structure for the uniform buffer object (now without fixed light arrays). */ struct UniformBufferObject { alignas(16) glm::mat4 model; @@ -69,9 +69,14 @@ struct UniformBufferObject { alignas(4) int lightCount; // Number of active lights (dynamic) alignas(4) int shadowMapCount; // Number of active shadow maps (dynamic) alignas(4) float shadowBias; // Shadow bias to prevent shadow acne - alignas(4) float padding; // Padding for alignment + alignas(4) float padding1; // Padding for alignment + + // Additional padding to ensure the structure size is aligned to 64 bytes (device nonCoherentAtomSize) + // Current size: 3*64 + 16 + 8*4 = 240 bytes, pad to 256 bytes (multiple of 64) + alignas(4) float padding2[4]; // Add 16 more bytes to reach 256 bytes total }; + /** * @brief Structure for PBR material properties. * This structure must match the PushConstants structure in the PBR shader. @@ -157,6 +162,7 @@ class Renderer { */ bool IsInitialized() const { return initialized; } + /** * @brief Get the Vulkan device. * @return The Vulkan device. @@ -184,7 +190,7 @@ class Renderer { * @param properties The memory properties. * @return The memory type index. */ - uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { + uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { return findMemoryType(typeFilter, properties); } @@ -201,14 +207,13 @@ class Renderer { * @param commandBuffer The command buffer to submit. * @param fence The fence to signal when the operation completes. */ - void SubmitToComputeQueue(vk::CommandBuffer commandBuffer, vk::Fence fence) { - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &commandBuffer - }; - + void SubmitToComputeQueue(vk::CommandBuffer commandBuffer, vk::Fence fence) const { // Use mutex to ensure thread-safe access to compute queue { + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer + }; std::lock_guard lock(queueMutex); computeQueue.submit(submitInfo, fence); } @@ -234,7 +239,7 @@ class Renderer { } /** - * @brief Load a texture from file. + * @brief Load a texture from a file. * @param texturePath The path to the texture file. * @return True if the texture was loaded successfully, false otherwise. */ @@ -270,8 +275,8 @@ class Renderer { * @param width The image width. * @param height The image height. */ - void CopyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { - // Create default single region for backward compatibility + void CopyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) const { + // Create a default single region for backward compatibility std::vector regions = {{ .bufferOffset = 0, .bufferRowLength = 0, @@ -314,10 +319,10 @@ class Renderer { /** * @brief Set the model loader reference for accessing extracted lights. - * @param modelLoader Pointer to the model loader. + * @param _modelLoader Pointer to the model loader. */ - void SetModelLoader(class ModelLoader* modelLoader) { - this->modelLoader = modelLoader; + void SetModelLoader(ModelLoader* _modelLoader) { + modelLoader = _modelLoader; } /** @@ -328,18 +333,18 @@ class Renderer { /** * @brief Set the gamma correction value for PBR rendering. - * @param gamma The gamma correction value (typically 2.2). + * @param _gamma The gamma correction value (typically 2.2). */ - void SetGamma(float gamma) { - this->gamma = gamma; + void SetGamma(float _gamma) { + gamma = _gamma; } /** * @brief Set the exposure value for HDR tone mapping. - * @param exposure The exposure value (1.0 = no adjustment). + * @param _exposure The exposure value (1.0 = no adjustment). */ - void SetExposure(float exposure) { - this->exposure = exposure; + void SetExposure(float _exposure) { + exposure = _exposure; } /** @@ -357,14 +362,6 @@ class Renderer { } } - /** - * @brief Set the entity to be highlighted during rendering. - * @param entity The entity to highlight, or nullptr to clear highlighting. - */ - void SetHighlightedEntity(Entity* entity) { - this->highlightedEntity = entity; - } - /** * @brief Create or resize light storage buffers to accommodate the given number of lights. * @param lightCount The number of lights to accommodate. @@ -373,7 +370,7 @@ class Renderer { bool createOrResizeLightStorageBuffers(size_t lightCount); /** - * @brief Update light storage buffer with current light data. + * @brief Update the light storage buffer with current light data. * @param frameIndex The current frame index. * @param lights The light data to upload. * @return True if successful, false otherwise. @@ -404,7 +401,7 @@ class Renderer { static const std::string SHARED_BRIGHT_RED_ID; /** - * @brief Determine appropriate texture format based on texture type. + * @brief Determine the appropriate texture format based on the texture type. * @param textureId The texture identifier to analyze. * @return The appropriate Vulkan format (sRGB for baseColor, linear for others). */ @@ -422,9 +419,6 @@ class Renderer { float exposure = 3.0f; // HDR exposure value (higher for emissive lighting) bool shadowsEnabled = true; // Shadow rendering enabled by default - // Entity highlighting - Entity* highlightedEntity = nullptr; - // Vulkan RAII context vk::raii::Context context; @@ -538,9 +532,13 @@ class Renderer { std::vector shadowMaps; // One shadow map per light // Shadow mapping constants - static constexpr uint32_t MAX_SHADOW_MAPS = 16; // Match actual shadow map count for performance + static constexpr uint32_t MAX_SHADOW_MAPS = 16; // Descriptors/array size remains 16 static constexpr uint32_t DEFAULT_SHADOW_MAP_SIZE = 2048; + // Performance clamps (to reduce per-frame cost) + static constexpr uint32_t MAX_ACTIVE_LIGHTS = 32; // Limit the number of lights processed per frame + static constexpr uint32_t MAX_SHADOW_MAPS_USED = 4; // Limit the number of shadows sampled per frame + // Static lights loaded during model initialization std::vector staticLights; @@ -561,6 +559,11 @@ class Renderer { std::vector uniformBuffersMapped; std::vector basicDescriptorSets; // For basic pipeline std::vector pbrDescriptorSets; // For PBR pipeline + + // Instance buffer for instanced rendering + vk::raii::Buffer instanceBuffer = nullptr; + std::unique_ptr instanceBufferAllocation = nullptr; + void* instanceBufferMapped = nullptr; }; std::unordered_map entityResources; @@ -616,6 +619,7 @@ class Renderer { bool createDescriptorSetLayout(); bool createPBRDescriptorSetLayout(); bool createGraphicsPipeline(); + bool createPBRPipeline(); bool createLightingPipeline(); bool createComputePipeline(); @@ -647,6 +651,8 @@ class Renderer { void recreateSwapChain(); void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera); + void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera, const glm::mat4& customTransform); + void updateUniformBufferInternal(uint32_t currentImage, Entity* entity, CameraComponent* camera, UniformBufferObject& ubo); vk::raii::ShaderModule createShaderModule(const std::vector& code); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 9ed2b274..76f1805d 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -64,7 +65,7 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Pick physical device + // Pick the physical device if (!pickPhysicalDevice()) { return false; } @@ -107,12 +108,12 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Create descriptor set layout + // Create the descriptor set layout if (!createDescriptorSetLayout()) { return false; } - // Create graphics pipeline + // Create the graphics pipeline if (!createGraphicsPipeline()) { return false; } @@ -122,7 +123,7 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Create lighting pipeline + // Create the lighting pipeline if (!createLightingPipeline()) { std::cerr << "Failed to create lighting pipeline" << std::endl; return false; @@ -134,7 +135,7 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Create command pool + // Create the command pool if (!createCommandPool()) { return false; } @@ -144,7 +145,7 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Create descriptor pool + // Create the descriptor pool if (!createDescriptorPool()) { return false; } @@ -235,7 +236,7 @@ bool Renderer::createInstance(const std::string& appName, bool enableValidationL std::vector extensions; // Add required extensions for GLFW -#if PLATFORM_DESKTOP +#if defined(PLATFORM_DESKTOP) uint32_t glfwExtensionCount = 0; const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); @@ -341,57 +342,89 @@ bool Renderer::pickPhysicalDevice() { return false; } - // Find a suitable device using modern C++ ranges - const auto devIter = std::ranges::find_if( - devices, - [&](auto& device) { - // Print device properties for debugging - vk::PhysicalDeviceProperties deviceProperties = device.getProperties(); - std::cout << "Checking device: " << deviceProperties.deviceName << std::endl; - - // Check if device supports Vulkan 1.3 - bool supportsVulkan1_3 = deviceProperties.apiVersion >= VK_API_VERSION_1_3; - if (!supportsVulkan1_3) { - std::cout << " - Does not support Vulkan 1.3" << std::endl; - } + // Prioritize discrete GPUs (like NVIDIA RTX 2080) over integrated GPUs (like Intel UHD Graphics) + // First, collect all suitable devices with their suitability scores + std::multimap suitableDevices; + + for (auto& _device : devices) { + // Print device properties for debugging + vk::PhysicalDeviceProperties deviceProperties = _device.getProperties(); + std::cout << "Checking device: " << deviceProperties.deviceName + << " (Type: " << vk::to_string(deviceProperties.deviceType) << ")" << std::endl; + + // Check if the device supports Vulkan 1.3 + bool supportsVulkan1_3 = deviceProperties.apiVersion >= VK_API_VERSION_1_3; + if (!supportsVulkan1_3) { + std::cout << " - Does not support Vulkan 1.3" << std::endl; + continue; + } - // Check queue families - QueueFamilyIndices indices = findQueueFamilies(device); - bool supportsGraphics = indices.isComplete(); - if (!supportsGraphics) { - std::cout << " - Missing required queue families" << std::endl; - } + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(_device); + bool supportsGraphics = indices.isComplete(); + if (!supportsGraphics) { + std::cout << " - Missing required queue families" << std::endl; + continue; + } - // Check device extensions - bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(device); - if (!supportsAllRequiredExtensions) { - std::cout << " - Missing required extensions" << std::endl; - } + // Check device extensions + bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(_device); + if (!supportsAllRequiredExtensions) { + std::cout << " - Missing required extensions" << std::endl; + continue; + } - // Check swap chain support - bool swapChainAdequate = false; - if (supportsAllRequiredExtensions) { - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); - swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); - if (!swapChainAdequate) { - std::cout << " - Inadequate swap chain support" << std::endl; - } - } + // Check swap chain support + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(_device); + bool swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + if (!swapChainAdequate) { + std::cout << " - Inadequate swap chain support" << std::endl; + continue; + } - // Check for required features - auto features = device.template getFeatures2(); - bool supportsRequiredFeatures = features.template get().dynamicRendering; - if (!supportsRequiredFeatures) { - std::cout << " - Does not support required features (dynamicRendering)" << std::endl; + // Check for required features + auto features = _device.getFeatures2(); + bool supportsRequiredFeatures = features.get().dynamicRendering; + if (!supportsRequiredFeatures) { + std::cout << " - Does not support required features (dynamicRendering)" << std::endl; + continue; + } + + // Calculate suitability score - prioritize discrete GPUs + int score = 0; + + // Discrete GPUs get the highest priority (NVIDIA RTX 2080, AMD, etc.) + if (deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { + score += 1000; + std::cout << " - Discrete GPU: +1000 points" << std::endl; + } + // Integrated GPUs get lower priority (Intel UHD Graphics, etc.) + else if (deviceProperties.deviceType == vk::PhysicalDeviceType::eIntegratedGpu) { + score += 100; + std::cout << " - Integrated GPU: +100 points" << std::endl; + } + + // Add points for memory size (more VRAM is better) + vk::PhysicalDeviceMemoryProperties memProperties = _device.getMemoryProperties(); + for (uint32_t i = 0; i < memProperties.memoryHeapCount; i++) { + if (memProperties.memoryHeaps[i].flags & vk::MemoryHeapFlagBits::eDeviceLocal) { + // Add 1 point per GB of VRAM + score += static_cast(memProperties.memoryHeaps[i].size / (1024 * 1024 * 1024)); + break; } + } - return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && swapChainAdequate && supportsRequiredFeatures; - }); + std::cout << " - Device is suitable with score: " << score << std::endl; + suitableDevices.emplace(score, _device); + } - if (devIter != devices.end()) { - physicalDevice = *devIter; + if (!suitableDevices.empty()) { + // Select the device with the highest score (discrete GPU with most VRAM) + physicalDevice = suitableDevices.rbegin()->second; vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); - std::cout << "Selected device: " << deviceProperties.deviceName << std::endl; + std::cout << "Selected device: " << deviceProperties.deviceName + << " (Type: " << vk::to_string(deviceProperties.deviceType) + << ", Score: " << suitableDevices.rbegin()->first << ")" << std::endl; // Store queue family indices for the selected device queueFamilyIndices = findQueueFamilies(physicalDevice); diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index 7e415f0f..c40e2be6 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -27,7 +27,7 @@ bool Renderer::createDescriptorSetLayout() { .pImmutableSamplers = nullptr }; - // Create descriptor set layout + // Create a descriptor set layout std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; vk::DescriptorSetLayoutCreateInfo layoutInfo{ .bindingCount = static_cast(bindings.size()), @@ -152,15 +152,30 @@ bool Renderer::createGraphicsPipeline() { vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - // Create vertex input info - auto bindingDescription = Vertex::getBindingDescription(); - auto attributeDescriptions = Vertex::getAttributeDescriptions(); + // Create vertex input info with instancing support + auto vertexBindingDescription = Vertex::getBindingDescription(); + auto instanceBindingDescription = InstanceData::getBindingDescription(); + std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription + }; + + auto vertexAttributeDescriptions = Vertex::getAttributeDescriptions(); + auto instanceAttributeDescriptions = InstanceData::getAttributeDescriptions(); + + // Combine all attribute descriptions (no duplicates) + std::vector allAttributeDescriptions; + allAttributeDescriptions.insert(allAttributeDescriptions.end(), vertexAttributeDescriptions.begin(), vertexAttributeDescriptions.end()); + allAttributeDescriptions.insert(allAttributeDescriptions.end(), instanceAttributeDescriptions.begin(), instanceAttributeDescriptions.end()); + + // Note: materialIndex attribute (Location 11) is not used by current shaders + // Removed to fix validation layer error - shaders don't expect input at location 11 vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = 1, - .pVertexBindingDescriptions = &bindingDescription, - .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), - .pVertexAttributeDescriptions = attributeDescriptions.data() + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), + .vertexAttributeDescriptionCount = static_cast(allAttributeDescriptions.size()), + .pVertexAttributeDescriptions = allAttributeDescriptions.data() }; // Create input assembly info @@ -250,7 +265,7 @@ bool Renderer::createGraphicsPipeline() { .stencilAttachmentFormat = vk::Format::eUndefined }; - // Create graphics pipeline + // Create the graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo{ .sType = vk::StructureType::eGraphicsPipelineCreateInfo, .pNext = &mainPipelineRenderingCreateInfo, @@ -309,50 +324,30 @@ bool Renderer::createPBRPipeline() { vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - // Define vertex binding description - vk::VertexInputBindingDescription bindingDescription{ - .binding = 0, - .stride = sizeof(float) * (3 + 3 + 2 + 4), // Position(3) + Normal(3) + UV(2) + Tangent(4) - .inputRate = vk::VertexInputRate::eVertex + // Define vertex and instance binding descriptions + auto vertexBindingDescription = Vertex::getBindingDescription(); + auto instanceBindingDescription = InstanceData::getBindingDescription(); + std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription }; - // Define vertex attribute descriptions - std::array attributeDescriptions = { - // Position attribute - vk::VertexInputAttributeDescription{ - .location = 0, - .binding = 0, - .format = vk::Format::eR32G32B32Sfloat, - .offset = 0 - }, - // Normal attribute - vk::VertexInputAttributeDescription{ - .location = 1, - .binding = 0, - .format = vk::Format::eR32G32B32Sfloat, - .offset = sizeof(float) * 3 - }, - // UV attribute - vk::VertexInputAttributeDescription{ - .location = 2, - .binding = 0, - .format = vk::Format::eR32G32Sfloat, - .offset = sizeof(float) * 6 - }, - // Tangent attribute - vk::VertexInputAttributeDescription{ - .location = 3, - .binding = 0, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = sizeof(float) * 8 - } - }; + // Define vertex and instance attribute descriptions + auto vertexAttributeDescriptions = Vertex::getAttributeDescriptions(); + auto instanceModelMatrixAttributes = InstanceData::getModelMatrixAttributeDescriptions(); + auto instanceNormalMatrixAttributes = InstanceData::getNormalMatrixAttributeDescriptions(); + + // Combine all attribute descriptions + std::vector allAttributeDescriptions; + allAttributeDescriptions.insert(allAttributeDescriptions.end(), vertexAttributeDescriptions.begin(), vertexAttributeDescriptions.end()); + allAttributeDescriptions.insert(allAttributeDescriptions.end(), instanceModelMatrixAttributes.begin(), instanceModelMatrixAttributes.end()); + allAttributeDescriptions.insert(allAttributeDescriptions.end(), instanceNormalMatrixAttributes.begin(), instanceNormalMatrixAttributes.end()); vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = 1, - .pVertexBindingDescriptions = &bindingDescription, - .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), - .pVertexAttributeDescriptions = attributeDescriptions.data() + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), + .vertexAttributeDescriptionCount = static_cast(allAttributeDescriptions.size()), + .pVertexAttributeDescriptions = allAttributeDescriptions.data() }; // Create input assembly info @@ -435,6 +430,15 @@ bool Renderer::createPBRPipeline() { pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + // Enable alpha blending for translucency (glass) + colorBlendAttachment.blendEnable = VK_TRUE; + colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; + colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd; + colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; + colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd; + // Create pipeline rendering info vk::Format depthFormat = findDepthFormat(); diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index cc0ae561..065e1ce6 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -313,6 +313,26 @@ void Renderer::recreateSwapChain() { createGraphicsPipeline(); createPBRPipeline(); createLightingPipeline(); + + // Recreate descriptor sets for all entities after swapchain/pipeline rebuild + for (auto& [entity, resources] : entityResources) { + if (!entity) continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) continue; + + std::string texturePath = meshComponent->GetTexturePath(); + // Fallback for basic pipeline: use baseColor when legacy path is empty + if (texturePath.empty()) { + const std::string& baseColor = meshComponent->GetBaseColorTexturePath(); + if (!baseColor.empty()) { + texturePath = baseColor; + } + } + // Recreate basic descriptor sets (ignore failures here to avoid breaking resize) + createDescriptorSets(entity, texturePath, false); + // Recreate PBR descriptor sets + createDescriptorSets(entity, texturePath, true); + } } // Update uniform buffer @@ -336,20 +356,52 @@ void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, Camera ubo.proj = camera->GetProjectionMatrix(); ubo.proj[1][1] *= -1; // Flip Y for Vulkan + // Continue with the rest of the uniform buffer setup + updateUniformBufferInternal(currentImage, entity, camera, ubo); +} + +// Overloaded version that accepts a custom transform matrix +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera, const glm::mat4& customTransform) { + // Create the uniform buffer object with custom transform + UniformBufferObject ubo{}; + ubo.model = customTransform; + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + ubo.proj[1][1] *= -1; // Flip Y for Vulkan + + // Continue with the rest of the uniform buffer setup + updateUniformBufferInternal(currentImage, entity, camera, ubo); +} + +// Internal helper function to complete uniform buffer setup +void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity, CameraComponent* camera, UniformBufferObject& ubo) { + // Get entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + return; + } + // Use static lights loaded during model initialization const std::vector& extractedLights = staticLights; if (!extractedLights.empty()) { - // Use all available lights (no hardcoded limit) - size_t numLights = extractedLights.size(); + // Limit the number of active lights for performance + size_t numLights = std::min(extractedLights.size(), size_t(MAX_ACTIVE_LIGHTS)); + + // Create a subset of lights to upload this frame + std::vector lightsSubset; + lightsSubset.reserve(numLights); + for (size_t i = 0; i < numLights; ++i) { + lightsSubset.push_back(extractedLights[i]); + } - // Update the light storage buffer with all light data - updateLightStorageBuffer(currentImage, extractedLights); + // Update the light storage buffer with the subset of light data + updateLightStorageBuffer(currentImage, lightsSubset); ubo.lightCount = static_cast(numLights); - // Set shadow map count based on shadowsEnabled flag + // Set shadow map count based on shadowsEnabled flag with a tighter cap if (shadowsEnabled) { - ubo.shadowMapCount = static_cast(std::min(numLights, size_t(16))); // Limit shadow maps to 16 for performance (indices 0-15) + ubo.shadowMapCount = static_cast(std::min(numLights, size_t(MAX_SHADOW_MAPS_USED))); } else { ubo.shadowMapCount = 0; // Disable shadows when shadowsEnabled is false } @@ -397,9 +449,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Acquire the next image from the swap chain uint32_t imageIndex; - // Use currentFrame for semaphore indexing to ensure consistency - uint32_t semaphoreIndex = currentFrame % imageAvailableSemaphores.size(); - auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[semaphoreIndex]); + // Use currentFrame for consistent semaphore indexing throughout acquire/submit/present chain + auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); imageIndex = result.second; // Check if the swap chain needs to be recreated @@ -489,6 +540,11 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam } } + // Skip camera entities - they should not be rendered + if (entity->GetName() == "Camera") { + continue; + } + // Get the mesh component auto meshComponent = entity->GetComponent(); if (!meshComponent) { @@ -531,13 +587,14 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam currentLayout = selectedLayout; } - // Update the uniform buffer - updateUniformBuffer(currentFrame, entity, camera); + // Always bind both vertex and instance buffers since shaders expect instance data + // The instancing toggle controls the rendering behavior, not the buffer binding + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); - // Bind the vertex buffer - std::array vertexBuffers = {*meshIt->second.vertexBuffer}; - std::array offsets = {0}; - commandBuffers[currentFrame].bindVertexBuffers(0, vertexBuffers, offsets); + // Always set UBO.model from the entity's transform; shaders combine it with instance matrices + updateUniformBuffer(currentFrame, entity, camera); // Bind the index buffer commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); @@ -585,50 +642,38 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { // Extract material name from entity name for any GLTF model entities std::string entityName = entity->GetName(); - size_t materialStart = entityName.find("_Material_"); - if (materialStart != std::string::npos) { - // Try to extract material name from entity name - size_t nameStart = entityName.find_last_of("_"); - if (nameStart != std::string::npos && nameStart < entityName.length() - 1) { - std::string materialName = entityName.substr(nameStart + 1); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + // After the tag, there should be a numeric material index, then an underscore, then the material name + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); Material* material = modelLoader->GetMaterial(materialName); if (material) { // Use actual PBR properties from the GLTF material - pushConstants.baseColorFactor = glm::vec4(material->albedo, 1.0f); + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); pushConstants.metallicFactor = material->metallic; pushConstants.roughnessFactor = material->roughness; pushConstants.emissiveFactor = material->emissive; // Set emissive factor for HDR emissive sources pushConstants.emissiveStrength = material->emissiveStrength; // Set emissive strength from KHR_materials_emissive_strength extension } else { - // Fallback: Use entity-specific variation - size_t hash = std::hash{}(entityName); - float variation = (hash % 100) / 100.0f; - pushConstants.baseColorFactor = glm::vec4(0.7f + variation * 0.3f, 0.6f + variation * 0.4f, 0.5f + variation * 0.5f, 1.0f); - pushConstants.metallicFactor = variation * 0.8f; - pushConstants.roughnessFactor = 0.2f + variation * 0.6f; - pushConstants.emissiveFactor = glm::vec3(0.0f); // No emissive for fallback materials - pushConstants.emissiveStrength = 1.0f; // Default emissive strength + // Default PBR material properties + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; } } } } else { - // Default PBR material properties for non-Bistro entities + // Default PBR material properties for non-GLTF entities pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); pushConstants.metallicFactor = 0.1f; pushConstants.roughnessFactor = 0.7f; - pushConstants.emissiveFactor = glm::vec3(0.0f); // No emissive for default materials - pushConstants.emissiveStrength = 1.0f; // Default emissive strength - } - - // Apply highlighting effect if this entity is highlighted - if (highlightedEntity == entity) { - // Add a bright yellow tint to highlight the entity - pushConstants.baseColorFactor.r = std::min(1.0f, pushConstants.baseColorFactor.r + 0.3f); - pushConstants.baseColorFactor.g = std::min(1.0f, pushConstants.baseColorFactor.g + 0.3f); - pushConstants.baseColorFactor.b = std::min(1.0f, pushConstants.baseColorFactor.b * 0.7f); // Reduce blue for yellow tint - // Also add some emissive glow for extra visibility - pushConstants.emissiveFactor = glm::vec3(0.2f, 0.2f, 0.0f); - pushConstants.emissiveStrength = 2.0f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 0.0f; } // Set texture binding indices @@ -649,8 +694,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam ); } - // Draw the entity once (no redundant draw calls) - commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, 1, 0, 0, 0); + uint32_t instanceCount = static_cast(std::max(1u, static_cast(meshComponent->GetInstanceCount()))); + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); } // Render ImGui if provided @@ -695,12 +740,12 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; vk::SubmitInfo submitInfo{ .waitSemaphoreCount = 1, - .pWaitSemaphores = &*imageAvailableSemaphores[semaphoreIndex], + .pWaitSemaphores = &*imageAvailableSemaphores[currentFrame], .pWaitDstStageMask = waitStages, .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, - .pSignalSemaphores = &*renderFinishedSemaphores[semaphoreIndex] + .pSignalSemaphores = &*renderFinishedSemaphores[currentFrame] }; // Use mutex to ensure thread-safe access to graphics queue @@ -712,7 +757,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Present the image vk::PresentInfoKHR presentInfo{ .waitSemaphoreCount = 1, - .pWaitSemaphores = &*renderFinishedSemaphores[semaphoreIndex], + .pWaitSemaphores = &*renderFinishedSemaphores[currentFrame], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 28b51051..71f44ff2 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1,15 +1,15 @@ #include "renderer.h" #include "model_loader.h" #include "mesh_component.h" +#include "transform_component.h" #include #include #include #include +#include #include -// Define STB_IMAGE_IMPLEMENTATION before including stb_image.h to provide the implementation -#define STB_IMAGE_IMPLEMENTATION -#include +// stb_image dependency removed; all GLTF textures are uploaded via memory path from ModelLoader. // KTX2 support #include @@ -63,23 +63,62 @@ bool Renderer::createDepthResources() { } // Create texture image -bool Renderer::createTextureImage(const std::string& texturePath, TextureResources& resources) { +bool Renderer::createTextureImage(const std::string& texturePath_, TextureResources& resources) { try { + auto texturePath = const_cast(texturePath_); // Check if texture already exists auto it = textureResources.find(texturePath); if (it != textureResources.end()) { - resources = std::move(it->second); + // Texture already loaded and cached; leave cache intact and return success return true; } // Check if this is a KTX2 file bool isKtx2 = texturePath.find(".ktx2") != std::string::npos; + // If it's a KTX2 texture but the path doesn't exist, try common fallback filename variants + if (isKtx2) { + std::filesystem::path origPath(texturePath); + if (!std::filesystem::exists(origPath)) { + std::string fname = origPath.filename().string(); + std::string dir = origPath.parent_path().string(); + auto tryCandidate = [&](const std::string& candidateName) -> bool { + std::filesystem::path cand = std::filesystem::path(dir) / candidateName; + if (std::filesystem::exists(cand)) { + std::cout << "Resolved missing texture '" << texturePath << "' to existing file '" << cand.string() << "'" << std::endl; + texturePath = cand.string(); + return true; + } + return false; + }; + // Known suffix variants near the end of filename before extension + // Examples: *_c.ktx2, *_d.ktx2, *_cm.ktx2, *_diffuse.ktx2, *_basecolor.ktx2, *_albedo.ktx2 + std::vector suffixes = {"_c", "_d", "_cm", "_diffuse", "_basecolor", "_albedo"}; + // If filename matches one known suffix, try others + for (const auto& s : suffixes) { + std::string key = s + ".ktx2"; + if (fname.size() > key.size() && fname.rfind(key) == fname.size() - key.size()) { + std::string prefix = fname.substr(0, fname.size() - key.size()); + for (const auto& alt : suffixes) { + if (alt == s) continue; + std::string candName = prefix + alt + ".ktx2"; + if (tryCandidate(candName)) { isKtx2 = true; break; } + } + break; // Only replace last suffix occurrence + } + } + } + } + int texWidth, texHeight, texChannels; - stbi_uc* pixels = nullptr; + unsigned char* pixels = nullptr; ktxTexture2* ktxTex = nullptr; vk::DeviceSize imageSize; + // Track KTX2 transcoding state and original format across the function scope + bool wasTranscoded = false; + VkFormat ktxHeaderVkFormat = VK_FORMAT_UNDEFINED; + uint32_t mipLevels = 1; std::vector copyRegions; @@ -89,12 +128,61 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); if (result != KTX_SUCCESS) { - std::cerr << "Failed to load KTX2 texture: " << texturePath << " (error: " << result << ")" << std::endl; - return false; + // Retry with sibling suffix variants if file exists but cannot be parsed/opened + std::filesystem::path origPath(texturePath); + std::string fname = origPath.filename().string(); + std::string dir = origPath.parent_path().string(); + auto tryLoad = [&](const std::string& candidateName) -> bool { + std::filesystem::path cand = std::filesystem::path(dir) / candidateName; + if (std::filesystem::exists(cand)) { + std::string candStr = cand.string(); + std::cout << "Retrying KTX2 load with sibling candidate '" << candStr << "' for original '" << texturePath << "'" << std::endl; + result = ktxTexture2_CreateFromNamedFile(candStr.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result == KTX_SUCCESS) { + texturePath = candStr; // Use the successfully opened candidate + return true; + } + } + return false; + }; + // Known suffix variants near the end of filename before extension + std::vector suffixes = {"_c", "_d", "_cm", "_diffuse", "_basecolor", "_albedo"}; + for (const auto& s : suffixes) { + std::string key = s + ".ktx2"; + if (fname.size() > key.size() && fname.rfind(key) == fname.size() - key.size()) { + std::string prefix = fname.substr(0, fname.size() - key.size()); + bool loaded = false; + for (const auto& alt : suffixes) { + if (alt == s) continue; + std::string candName = prefix + alt + ".ktx2"; + if (tryLoad(candName)) { loaded = true; break; } + } + if (loaded) break; + } + } + if (result != KTX_SUCCESS) { + // Last-ditch fallback: try alternate image formats (.png/.jpg/.jpeg) with same basename + std::filesystem::path orig = std::filesystem::path(texturePath); + std::vector altExts = {".png", ".jpg", ".jpeg", ".PNG", ".JPG", ".JPEG", ".dds", ".DDS", ".ktx", ".KTX"}; + for (const auto& ext : altExts) { + std::filesystem::path cand = orig; + cand.replace_extension(ext); + if (std::filesystem::exists(cand)) { + // Alternate non-KTX fallback removed per simplification policy + (void)ext; (void)orig; (void)cand; // suppress unused warnings + } + } + std::cerr << "Failed to load KTX2 texture: " << texturePath << " (error: " << result << ")" << std::endl; + return false; + } } + ktx2_or_alt_loaded:; + + // Cache header-provided VkFormat + ktxHeaderVkFormat = static_cast(ktxTex->vkFormat); // Check if texture needs transcoding (Basis Universal compressed) - bool wasTranscoded = ktxTexture2_NeedsTranscoding(ktxTex); + wasTranscoded = ktxTexture2_NeedsTranscoding(ktxTex); if (wasTranscoded) { // Transcode to RGBA8 uncompressed format for Vulkan compatibility ktx_transcode_fmt_e transcodeFormat = KTX_TTF_RGBA32; @@ -110,7 +198,8 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc texWidth = ktxTex->baseWidth; texHeight = ktxTex->baseHeight; texChannels = 4; // KTX2 textures are typically RGBA - // Disable mipmapping - only use base level (level 0) + // Disable mipmapping for now - memory pool only supports single mip level + // TODO: Implement proper mipmap support in memory pool mipLevels = 1; // Calculate size for base level only @@ -134,32 +223,10 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc .imageOffset = {0, 0, 0}, .imageExtent = {static_cast(texWidth), static_cast(texHeight), 1} }); - - std::cout << "Loaded KTX2 texture: " << texturePath - << " (" << texWidth << "x" << texHeight << ", base level only, size: " << imageSize << " bytes)" << std::endl; } else { - // Load standard image formats using stb_image - pixels = stbi_load(texturePath.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); - if (!pixels) { - std::cerr << "Failed to load texture image: " << texturePath << std::endl; - return false; - } - imageSize = texWidth * texHeight * 4; - - // Create single copy region for non-KTX2 textures - copyRegions.push_back({ - .bufferOffset = 0, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = { - .aspectMask = vk::ImageAspectFlagBits::eColor, - .mipLevel = 0, - .baseArrayLayer = 0, - .layerCount = 1 - }, - .imageOffset = {0, 0, 0}, - .imageExtent = {static_cast(texWidth), static_cast(texHeight), 1} - }); + // Non-KTX texture loading via file path is disabled to simplify pipeline. + std::cerr << "Unsupported non-KTX2 texture path: " << texturePath << std::endl; + return false; } // Create staging buffer @@ -203,13 +270,27 @@ bool Renderer::createTextureImage(const std::string& texturePath, TextureResourc if (isKtx2) { ktxTexture_Destroy((ktxTexture*)ktxTex); } else { - stbi_image_free(pixels); + // no-op: non-KTX path disabled } - // Determine appropriate texture format based on texture type - vk::Format textureFormat = Renderer::determineTextureFormat(texturePath); - std::cout << "Loading external texture " << texturePath << " with format: " - << (textureFormat == vk::Format::eR8G8B8A8Srgb ? "sRGB" : "Linear") << std::endl; + // Determine appropriate texture format based on texture type and KTX2 metadata + vk::Format textureFormat; + if (isKtx2) { + if (wasTranscoded) { + // For transcoded Basis to RGBA32, choose by heuristic (sRGB for baseColor/albedo/diffuse) + textureFormat = Renderer::determineTextureFormat(texturePath); + } else { + // Use the VkFormat provided by the KTX2 container if available (from header) + VkFormat vkfmt = ktxHeaderVkFormat; + if (vkfmt == VK_FORMAT_UNDEFINED) { + textureFormat = Renderer::determineTextureFormat(texturePath); + } else { + textureFormat = static_cast(vkfmt); + } + } + } else { + textureFormat = Renderer::determineTextureFormat(texturePath); + } // Create texture image using memory pool auto [textureImg, textureImgAllocation] = createImagePooled( @@ -294,8 +375,8 @@ bool Renderer::createTextureImageView(TextureResources& resources) { // Create shared default PBR textures (to avoid creating hundreds of identical textures) bool Renderer::createSharedDefaultPBRTextures() { try { - unsigned char whitePixel[4] = {128, 128, 128, 255}; // 50% gray instead of pure white - if (!LoadTextureFromMemory(SHARED_DEFAULT_ALBEDO_ID, whitePixel, 1, 1, 4)) { + unsigned char translucentPixel[4] = {128, 128, 128, 125}; // 50% alpha + if (!LoadTextureFromMemory(SHARED_DEFAULT_ALBEDO_ID, translucentPixel, 1, 1, 4)) { std::cerr << "Failed to create shared default albedo texture" << std::endl; return false; } @@ -335,7 +416,6 @@ bool Renderer::createSharedDefaultPBRTextures() { return false; } - std::cout << "Successfully created all shared default PBR textures" << std::endl; return true; } catch (const std::exception& e) { std::cerr << "Failed to create shared default PBR textures: " << e.what() << std::endl; @@ -447,7 +527,7 @@ bool Renderer::createTextureSampler(TextureResources& resources) { .addressModeW = vk::SamplerAddressMode::eRepeat, .mipLodBias = 0.0f, .anisotropyEnable = VK_TRUE, - .maxAnisotropy = properties.limits.maxSamplerAnisotropy, + .maxAnisotropy = std::min(properties.limits.maxSamplerAnisotropy, 8.0f), .compareEnable = VK_FALSE, .compareOp = vk::CompareOp::eAlways, .minLod = 0.0f, @@ -478,15 +558,13 @@ bool Renderer::LoadTexture(const std::string& texturePath) { return true; } - // Create temporary texture resources + // Create temporary texture resources (unused output; cache will be populated internally) TextureResources tempResources; - // Use existing createTextureImage method + // Use existing createTextureImage method (it inserts into textureResources on success) bool success = createTextureImage(texturePath, tempResources); - if (success) { - std::cout << "Successfully loaded texture: " << texturePath << std::endl; - } else { + if (!success) { std::cerr << "Failed to load texture: " << texturePath << std::endl; } @@ -495,10 +573,15 @@ bool Renderer::LoadTexture(const std::string& texturePath) { // Determine appropriate texture format based on texture type vk::Format Renderer::determineTextureFormat(const std::string& textureId) { - // BaseColor/Albedo textures should be in sRGB space for proper gamma correction - if (textureId.find("BaseColor") != std::string::npos || - textureId.find("Albedo") != std::string::npos || - textureId.find("Diffuse") != std::string::npos || + // Determine sRGB vs Linear in a case-insensitive way + std::string idLower = textureId; + std::transform(idLower.begin(), idLower.end(), idLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + + // BaseColor/Albedo/Diffuse textures should be in sRGB space for proper gamma correction + if (idLower.find("basecolor") != std::string::npos || + idLower.find("base_color") != std::string::npos || + idLower.find("albedo") != std::string::npos || + idLower.find("diffuse") != std::string::npos || textureId == Renderer::SHARED_DEFAULT_ALBEDO_ID) { return vk::Format::eR8G8B8A8Srgb; } @@ -771,6 +854,43 @@ bool Renderer::createUniformBuffers(Entity* entity) { resources.uniformBuffersMapped.emplace_back(mappedMemory); } + // Create instance buffer for all entities (shaders always expect instance data) + auto* meshComponent = entity->GetComponent(); + if (meshComponent) { + std::vector instanceData; + + // CRITICAL FIX: Check if entity has any instance data first + if (meshComponent->GetInstanceCount() > 0) { + // Use existing instance data from GLTF loading (whether 1 or many instances) + instanceData = meshComponent->GetInstances(); + } else { + // Create single instance data using IDENTITY matrix to avoid double-transform with UBO.model + InstanceData singleInstance; + singleInstance.setModelMatrix(glm::mat4(1.0f)); + instanceData = {singleInstance}; + } + + vk::DeviceSize instanceBufferSize = sizeof(InstanceData) * instanceData.size(); + + auto [instanceBuffer, instanceBufferAllocation] = createBufferPooled( + instanceBufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy instance data to buffer + void* instanceMappedMemory = instanceBufferAllocation->mappedPtr; + if (instanceMappedMemory) { + std::memcpy(instanceMappedMemory, instanceData.data(), instanceBufferSize); + } else { + std::cerr << "Warning: Instance buffer allocation is not mapped" << std::endl; + } + + resources.instanceBuffer = std::move(instanceBuffer); + resources.instanceBufferAllocation = std::move(instanceBufferAllocation); + resources.instanceBufferMapped = instanceMappedMemory; + } + // Add to entity resources map entityResources[entity] = std::move(resources); @@ -789,7 +909,7 @@ bool Renderer::createDescriptorPool() { // Each entity needs descriptor sets for both basic and PBR pipelines // PBR pipeline needs 7 descriptors per set (1 UBO + 5 PBR textures + 1 shadow map array with 16 shadow maps) // Basic pipeline needs 2 descriptors per set (1 UBO + 1 texture) - const uint32_t maxEntities = 2500; // Increased to 2500 entities to handle large models like bistro with extra safety margin + const uint32_t maxEntities = 20000; // Increased to 20k entities to handle large scenes like Bistro reliably const uint32_t maxDescriptorSets = MAX_FRAMES_IN_FLIGHT * maxEntities * 2; // 2 pipeline types per entity // Calculate descriptor counts @@ -848,26 +968,39 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa if (!texturePath.empty()) { auto textureIt = textureResources.find(texturePath); if (textureIt == textureResources.end()) { - // Try to create texture resources - TextureResources tempRes; - if (!createTextureImage(texturePath, tempRes)) { - // Texture loading failed - use default texture instead of failing descriptor set creation - std::cerr << "Warning: Failed to load texture " << texturePath - << " for entity " << entity->GetName() - << ", using default texture instead" << std::endl; - - // Use default texture resources instead - textureRes = &defaultTextureResources; - } else { - // Texture loaded successfully, find it in the map - textureIt = textureResources.find(texturePath); - if (textureIt == textureResources.end()) { - std::cerr << "Warning: Failed to find texture after creation: " << texturePath - << ", using default texture instead" << std::endl; - textureRes = &defaultTextureResources; + // If this is a GLTF embedded texture ID, don't try to load from disk + if (texturePath.rfind("gltf_", 0) == 0) { + // Handle both gltf_baseColor_{i} and gltf_basecolor_{i} + const std::string prefixUpper = "gltf_baseColor_"; + const std::string prefixLower = "gltf_basecolor_"; + if (texturePath.rfind(prefixUpper, 0) == 0 || texturePath.rfind(prefixLower, 0) == 0) { + const bool isUpper = texturePath.rfind(prefixUpper, 0) == 0; + std::string index = texturePath.substr((isUpper ? prefixUpper.size() : prefixLower.size())); + // Try direct baseColor id first + std::string baseColorId = "gltf_baseColor_" + index; + auto bcIt = textureResources.find(baseColorId); + if (bcIt != textureResources.end()) { + textureRes = &bcIt->second; + } else { + // Try alias to generic gltf_texture_{index} + std::string alias = "gltf_texture_" + index; + auto aliasIt = textureResources.find(alias); + if (aliasIt != textureResources.end()) { + textureRes = &aliasIt->second; + } else { + std::cerr << "Warning: Embedded texture not found: " << texturePath + << " (also missing alias: " << alias << ") using default." << std::endl; + textureRes = &defaultTextureResources; + } + } } else { - textureRes = &textureIt->second; + std::cerr << "Warning: Embedded texture not found: " << texturePath << ", using default." << std::endl; + textureRes = &defaultTextureResources; } + } else { + std::cerr << "Warning: On-demand texture loading disabled for " << texturePath + << "; using default texture instead" << std::endl; + textureRes = &defaultTextureResources; } } else { textureRes = &textureIt->second; @@ -891,19 +1024,53 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa // Only create descriptor sets if they don't already exist for this pipeline type if (targetDescriptorSets.empty()) { - // Allocate descriptor sets using RAII wrapper - vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); - - // Convert to vector of individual RAII descriptor sets - targetDescriptorSets.clear(); - targetDescriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - targetDescriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); + try { + // Allocate descriptor sets using RAII wrapper + vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); + + // Convert to vector of individual RAII descriptor sets + targetDescriptorSets.clear(); + targetDescriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + targetDescriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); + } + } catch (const std::exception& e) { + std::cerr << "Failed to allocate descriptor sets for entity " << entity->GetName() + << " (pipeline: " << (usePBR ? "PBR" : "basic") << "): " << e.what() << std::endl; + return false; } } + // Validate descriptor sets before using them + if (targetDescriptorSets.size() != MAX_FRAMES_IN_FLIGHT) { + std::cerr << "Invalid descriptor set count for entity " << entity->GetName() + << " (expected: " << MAX_FRAMES_IN_FLIGHT << ", got: " << targetDescriptorSets.size() << ")" << std::endl; + return false; + } + + // Validate default texture resources before using them + if (!*defaultTextureResources.textureSampler || !*defaultTextureResources.textureImageView) { + std::cerr << "Invalid default texture resources for entity " << entity->GetName() << std::endl; + return false; + } + // Update descriptor sets for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + // Validate descriptor set handle before using it + if (!*targetDescriptorSets[i]) { + std::cerr << "Invalid descriptor set handle for entity " << entity->GetName() + << " at frame " << i << " (pipeline: " << (usePBR ? "PBR" : "basic") << ")" << std::endl; + return false; + } + + // Validate uniform buffer before creating descriptor + if (i >= entityIt->second.uniformBuffers.size() || + !*entityIt->second.uniformBuffers[i]) { + std::cerr << "Invalid uniform buffer for entity " << entity->GetName() + << " at frame " << i << std::endl; + return false; + } + // Uniform buffer descriptor vk::DescriptorBufferInfo bufferInfo{ .buffer = *entityIt->second.uniformBuffers[i], @@ -929,10 +1096,64 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa // Get all PBR texture paths from the entity's MeshComponent auto meshComponent = entity->GetComponent(); + // Resolve baseColor path with multiple fallbacks: GLTF baseColor -> legacy texturePath -> material DB -> shared default + std::string resolvedBaseColor; + if (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) { + resolvedBaseColor = meshComponent->GetBaseColorTexturePath(); + } else if (meshComponent && !meshComponent->GetTexturePath().empty()) { + resolvedBaseColor = meshComponent->GetTexturePath(); + } else { + // Try to use material name from entity name to query ModelLoader + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + // Expect format: _Material__ + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + if (modelLoader) { + Material* mat = modelLoader->GetMaterial(materialName); + if (mat && !mat->albedoTexturePath.empty()) { + resolvedBaseColor = mat->albedoTexturePath; + } + } + } + } + if (resolvedBaseColor.empty()) { + resolvedBaseColor = SHARED_DEFAULT_ALBEDO_ID; + } + } + + // Heuristic: if still default and we have an external normal map like *_ddna.ktx2, try to guess base color sibling + if (resolvedBaseColor == SHARED_DEFAULT_ALBEDO_ID && meshComponent) { + std::string normalPath = meshComponent->GetNormalTexturePath(); + if (!normalPath.empty() && normalPath.rfind("gltf_", 0) != 0) { + // Make a lowercase copy for pattern checks + std::string normalLower = normalPath; + std::transform(normalLower.begin(), normalLower.end(), normalLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + if (normalLower.find("_ddna") != std::string::npos) { + // Try replacing _ddna with common diffuse/basecolor suffixes + std::vector suffixes = {"_d", "_c", "_cm", "_diffuse", "_basecolor"}; + for (const auto& suf : suffixes) { + std::string candidate = normalPath; + // Replace only the first occurrence of _ddna + size_t pos = normalLower.find("_ddna"); + if (pos != std::string::npos) { + candidate.replace(pos, 5, suf); + // Attempt to load; if successful, use this as resolved base color + // On-demand loading disabled; skip attempting to load candidate + (void)candidate; // suppress unused + break; + } + } + } + } + } + std::vector pbrTexturePaths = { - // Binding 1: baseColor - use GLTF texture or fallback to shared default - (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) ? - meshComponent->GetBaseColorTexturePath() : SHARED_DEFAULT_ALBEDO_ID, + // Binding 1: baseColor + resolvedBaseColor, // Binding 2: metallicRoughness - use GLTF texture or fallback to shared default (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) ? meshComponent->GetMetallicRoughnessTexturePath() : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID, @@ -956,21 +1177,69 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa if (textureIt != textureResources.end()) { // Use the specific texture for this binding const auto& texRes = textureIt->second; - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *texRes.textureSampler, - .imageView = *texRes.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; + + // Validate texture resources before using them (check if RAII objects are valid) + if (*texRes.textureSampler == VK_NULL_HANDLE || *texRes.textureImageView == VK_NULL_HANDLE) { + std::cerr << "Invalid texture resources for " << currentTexturePath + << " in entity " << entity->GetName() << ", using default texture" << std::endl; + // Fall back to default texture + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *texRes.textureSampler, + .imageView = *texRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } } else { - // Fall back to default white texture if the specific texture is not found - imageInfos[j] = vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; + // On-demand texture loading disabled; use alias or defaults below + // Try alias for embedded baseColor textures: gltf_baseColor_{i} -> gltf_texture_{i} + if (currentTexturePath.rfind("gltf_baseColor_", 0) == 0 || + currentTexturePath.rfind("gltf_basecolor_", 0) == 0) { + std::string prefix = (currentTexturePath.rfind("gltf_baseColor_", 0) == 0) + ? std::string("gltf_baseColor_") + : std::string("gltf_basecolor_"); + std::string index = currentTexturePath.substr(prefix.size()); + std::string alias = "gltf_texture_" + index; + auto aliasIt = textureResources.find(alias); + if (aliasIt != textureResources.end()) { + const auto& texRes = aliasIt->second; + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *texRes.textureSampler, + .imageView = *texRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + // Fall back to default white texture if the specific texture is not found + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + } else { + // Fall back to default white texture if the specific texture is not found + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } } + descriptor_path_resolved: ; } + // Debug: report resolved baseColor texture binding for this entity + if (meshComponent) { + const std::string& bc = pbrTexturePaths[0]; + bool present = textureResources.find(bc) != textureResources.end(); + std::cout << "PBR baseColor for entity '" << entity->GetName() << "': '" << bc + << "' (" << (present ? "loaded" : "not in cache") << ")" << std::endl; + } // Create descriptor writes for all 5 texture bindings for (int binding = 1; binding <= 5; binding++) { descriptorWrites[binding] = vk::WriteDescriptorSet{ @@ -1118,8 +1387,6 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa // Pre-allocate all Vulkan resources for an entity during scene loading bool Renderer::preAllocateEntityResources(Entity* entity) { try { - std::cout << "Pre-allocating resources for entity: " << entity->GetName() << std::endl; - // Get the mesh component auto meshComponent = entity->GetComponent(); if (!meshComponent) { @@ -1141,6 +1408,13 @@ bool Renderer::preAllocateEntityResources(Entity* entity) { // 3. Pre-allocate BOTH basic and PBR descriptor sets std::string texturePath = meshComponent->GetTexturePath(); + // Fallback: if legacy texturePath is empty, use PBR baseColor texture + if (texturePath.empty()) { + const std::string& baseColor = meshComponent->GetBaseColorTexturePath(); + if (!baseColor.empty()) { + texturePath = baseColor; + } + } // Create basic descriptor sets if (!createDescriptorSets(entity, texturePath, false)) { @@ -1153,8 +1427,6 @@ bool Renderer::preAllocateEntityResources(Entity* entity) { std::cerr << "Failed to create PBR descriptor sets for entity: " << entity->GetName() << std::endl; return false; } - - std::cout << "Successfully pre-allocated all resources for entity: " << entity->GetName() << std::endl; return true; } catch (const std::exception& e) { @@ -1175,7 +1447,6 @@ std::pair> Renderer::c // Use memory pool for allocation auto [buffer, allocation] = memoryPool->createBuffer(size, usage, properties); - std::cout << "Created buffer using memory pool: " << size << " bytes" << std::endl; return {std::move(buffer), std::move(allocation)}; @@ -1212,8 +1483,6 @@ std::pair Renderer::createBuffer( } try { - std::cout << "Creating staging buffer with direct allocation: " << size << " bytes" << std::endl; - vk::BufferCreateInfo bufferInfo{ .size = size, .usage = usage, @@ -1224,8 +1493,14 @@ std::pair Renderer::createBuffer( // Allocate memory directly for staging buffers only vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + // Align allocation size to nonCoherentAtomSize (64 bytes) to prevent validation errors + // VUID-VkMappedMemoryRange-size-01390 requires memory flush sizes to be multiples of nonCoherentAtomSize + const vk::DeviceSize nonCoherentAtomSize = 64; // Typical value, should query from device properties + vk::DeviceSize alignedSize = ((memRequirements.size + nonCoherentAtomSize - 1) / nonCoherentAtomSize) * nonCoherentAtomSize; + vk::MemoryAllocateInfo allocInfo{ - .allocationSize = memRequirements.size, + .allocationSize = alignedSize, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) }; @@ -1666,16 +1941,19 @@ bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector #include +#include #include #include +#include /** * @brief Calculate bounding box dimensions for a MaterialMesh. @@ -29,17 +31,6 @@ glm::vec3 CalculateBoundingBoxSize(const MaterialMesh& materialMesh) { return maxBounds - minBounds; } -/** - * @brief Determine if an object is considered "small" based on its bounding box. - * @param boundingBoxSize The size of the bounding box. - * @return True if the object is small, false otherwise. - */ -bool IsSmallObject(const glm::vec3& boundingBoxSize) { - // Consider an object "small" if its largest dimension is less than 8.0 units - float maxDimension = std::max({boundingBoxSize.x, boundingBoxSize.y, boundingBoxSize.z}); - return maxDimension < 0.1f; -} - /** * @brief Load a GLTF model synchronously on the main thread. * @param engine The engine to create entities in. @@ -50,8 +41,6 @@ bool IsSmallObject(const glm::vec3& boundingBoxSize) { */ void LoadGLTFModel(Engine* engine, const std::string& modelPath, const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale) { - std::cout << "Loading GLTF model synchronously on main thread: " << modelPath << std::endl; - // Get the model loader and renderer ModelLoader* modelLoader = engine->GetModelLoader(); Renderer* renderer = engine->GetRenderer(); @@ -78,7 +67,7 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Extract lights from the model and transform them to world space std::vector extractedLights = modelLoader->GetExtractedLights(modelPath); - // Create transformation matrix from position, rotation, and scale + // Create a transformation matrix from position, rotation, and scale glm::mat4 transformMatrix = glm::mat4(1.0f); transformMatrix = glm::translate(transformMatrix, position); transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); @@ -101,8 +90,6 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Extract and apply cameras from the GLTF model const std::vector& cameras = loadedModel->GetCameras(); if (!cameras.empty()) { - std::cout << "Found " << cameras.size() << " camera(s) in GLTF model, using the first one to replace default camera" << std::endl; - const CameraData& gltfCamera = cameras[0]; // Use the first camera // Find or create a camera entity to replace the default one @@ -127,38 +114,29 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Apply rotation from GLTF camera glm::vec3 eulerAngles = glm::eulerAngles(gltfCamera.rotation); cameraTransform->SetRotation(glm::degrees(eulerAngles)); - - std::cout << " Applied GLTF camera position: (" << worldPos.x << ", " << worldPos.y << ", " << worldPos.z << ")" << std::endl; - std::cout << " Applied GLTF camera rotation: (" << glm::degrees(eulerAngles.x) << ", " << glm::degrees(eulerAngles.y) << ", " << glm::degrees(eulerAngles.z) << ")" << std::endl; } // Update the camera component with GLTF properties auto* camera = cameraEntity->GetComponent(); if (camera) { + camera->ForceViewMatrixUpdate(); // Only sets viewMatrixDirty flag, doesn't change camera orientation if (gltfCamera.isPerspective) { camera->SetFieldOfView(glm::degrees(gltfCamera.fov)); // Convert radians to degrees camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); if (gltfCamera.aspectRatio > 0.0f) { camera->SetAspectRatio(gltfCamera.aspectRatio); } - std::cout << " Applied GLTF perspective camera: FOV=" << glm::degrees(gltfCamera.fov) - << ", Near=" << gltfCamera.nearPlane << ", Far=" << gltfCamera.farPlane << std::endl; } else { // Handle orthographic camera if needed camera->SetProjectionType(CameraComponent::ProjectionType::Orthographic); camera->SetOrthographicSize(gltfCamera.orthographicSize, gltfCamera.orthographicSize); camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); - std::cout << " Applied GLTF orthographic camera: Size=" << gltfCamera.orthographicSize - << ", Near=" << gltfCamera.nearPlane << ", Far=" << gltfCamera.farPlane << std::endl; } // Set this as the active camera engine->SetActiveCamera(camera); - std::cout << " Set GLTF camera as active camera" << std::endl; } } - } else { - std::cout << "No cameras found in GLTF model, keeping default camera" << std::endl; } // Get the material meshes from the loaded model @@ -168,53 +146,6 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, return; } - std::cout << "Creating " << materialMeshes.size() << " entities from loaded materials..." << std::endl; - - // First, collect and load all unique external texture files - std::set uniqueTextures; - for (const auto& materialMesh : materialMeshes) { - // Collect all texture types from this material - if (!materialMesh.baseColorTexturePath.empty()) { - uniqueTextures.insert(materialMesh.baseColorTexturePath); - } - if (!materialMesh.normalTexturePath.empty()) { - uniqueTextures.insert(materialMesh.normalTexturePath); - } - if (!materialMesh.metallicRoughnessTexturePath.empty()) { - uniqueTextures.insert(materialMesh.metallicRoughnessTexturePath); - } - if (!materialMesh.occlusionTexturePath.empty()) { - uniqueTextures.insert(materialMesh.occlusionTexturePath); - } - if (!materialMesh.emissiveTexturePath.empty()) { - uniqueTextures.insert(materialMesh.emissiveTexturePath); - } - // Also include legacy texturePath for backward compatibility - if (!materialMesh.texturePath.empty()) { - uniqueTextures.insert(materialMesh.texturePath); - } - } - - // Filter out embedded GLTF textures (already loaded in memory) and load only actual external files - std::set externalTextures; - for (const std::string& texturePath : uniqueTextures) { - // Skip embedded GLTF textures (they start with "gltf_texture_" and are already loaded in memory) - if (texturePath.find("gltf_texture_") != 0) { - externalTextures.insert(texturePath); - } - } - - if (!externalTextures.empty()) { - std::cout << "Loading " << externalTextures.size() << " unique external texture files..." << std::endl; - for (const std::string& texturePath : externalTextures) { - if (!renderer->LoadTexture(texturePath)) { - std::cerr << "Warning: Failed to load external texture: " << texturePath << std::endl; - } - } - } else { - std::cout << "No external texture files to load (all textures are embedded in GLTF)" << std::endl; - } - int entitiesCreated = 0; for (const auto& materialMesh : materialMeshes) { // Create an entity name based on model and material @@ -233,6 +164,15 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, mesh->SetVertices(materialMesh.vertices); mesh->SetIndices(materialMesh.indices); + if (materialMesh.GetInstanceCount() > 0) { + const std::vector& instances = materialMesh.instances; + for (const auto& instanceData : instances) { + // Reconstruct the transformation matrix from InstanceData column vectors + glm::mat4 instanceMatrix = instanceData.getModelMatrix(); + mesh->AddInstance(instanceMatrix, static_cast(materialMesh.materialIndex)); + } + } + // Set ALL PBR texture paths for this material // Set primary texture path for backward compatibility if (!materialMesh.texturePath.empty()) { @@ -256,6 +196,28 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, mesh->SetEmissiveTexturePath(materialMesh.emissiveTexturePath); } + // Fallback: Use material DB (from ModelLoader) if any PBR texture is still missing + if (modelLoader) { + Material* mat = modelLoader->GetMaterial(materialMesh.materialName); + if (mat) { + if (mesh->GetBaseColorTexturePath().empty() && !mat->albedoTexturePath.empty()) { + mesh->SetBaseColorTexturePath(mat->albedoTexturePath); + } + if (mesh->GetNormalTexturePath().empty() && !mat->normalTexturePath.empty()) { + mesh->SetNormalTexturePath(mat->normalTexturePath); + } + if (mesh->GetMetallicRoughnessTexturePath().empty() && !mat->metallicRoughnessTexturePath.empty()) { + mesh->SetMetallicRoughnessTexturePath(mat->metallicRoughnessTexturePath); + } + if (mesh->GetOcclusionTexturePath().empty() && !mat->occlusionTexturePath.empty()) { + mesh->SetOcclusionTexturePath(mat->occlusionTexturePath); + } + if (mesh->GetEmissiveTexturePath().empty() && !mat->emissiveTexturePath.empty()) { + mesh->SetEmissiveTexturePath(mat->emissiveTexturePath); + } + } + } + // Pre-allocate all Vulkan resources for this entity if (!renderer->preAllocateEntityResources(materialEntity)) { std::cerr << "Failed to pre-allocate resources for entity: " << entityName << std::endl; @@ -282,9 +244,6 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; } } - - std::cout << "Successfully created " << entitiesCreated << " entities from loaded materials" << std::endl; - } catch (const std::exception& e) { std::cerr << "Error loading GLTF model: " << e.what() << std::endl; } diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 70788717..fd3fa049 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -2,10 +2,14 @@ // Input from vertex buffer struct VSInput { - float3 Position : POSITION; - float3 Normal : NORMAL; - float2 UV : TEXCOORD0; - float4 Tangent : TANGENT; + [[vk::location(0)]] float3 Position; + [[vk::location(1)]] float3 Normal; + [[vk::location(2)]] float2 UV; + [[vk::location(3)]] float4 Tangent; + + // Per-instance data as true matrices + [[vk::location(4)]] column_major float4x4 InstanceModelMatrix; // binding 1 (uses 4 locations) + [[vk::location(8)]] column_major float4x3 InstanceNormalMatrix; // binding 1 (uses 3 locations) }; // Output from vertex shader / Input to fragment shader @@ -151,15 +155,20 @@ VSOutput VSMain(VSInput input) { VSOutput output; - // Transform position to clip space - float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + // Use instance matrices directly + float4x4 instanceModelMatrix = input.InstanceModelMatrix; + float3x3 normalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; + + // Transform position to world space: entity model * instance model + float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); // Pass world position to fragment shader output.WorldPos = worldPos.xyz; - // Transform normal to world space - output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + // Transform normal to world space using reconstructed normal matrix and entity model + float3x3 model3x3 = (float3x3)ubo.model; + output.Normal = normalize(mul(model3x3, mul(normalMatrix3x3, input.Normal))); // Pass texture coordinates output.UV = input.UV; @@ -174,19 +183,20 @@ VSOutput VSMain(VSInput input) [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET { - // Sample material textures - float4 baseColor = baseColorMap.Sample(input.UV) * material.baseColorFactor; - float2 metallicRoughness = metallicRoughnessMap.Sample(input.UV).bg; + // Sample material textures (flip V to match glTF UV origin) + float2 uv = float2(input.UV.x, 1.0 - input.UV.y); + float4 baseColor = baseColorMap.Sample(uv) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(uv).bg; float metallic = metallicRoughness.x * material.metallicFactor; float roughness = metallicRoughness.y * material.roughnessFactor; - float ao = occlusionMap.Sample(input.UV).r; - float3 emissive = emissiveMap.Sample(input.UV).rgb * material.emissiveFactor * material.emissiveStrength; + float ao = occlusionMap.Sample(uv).r; + float3 emissive = emissiveMap.Sample(uv).rgb * material.emissiveFactor * material.emissiveStrength; // Calculate normal in tangent space float3 N = normalize(input.Normal); if (material.normalTextureSet >= 0) { // Apply normal mapping - float3 tangentNormal = normalMap.Sample(input.UV).xyz * 2.0 - 1.0; + float3 tangentNormal = normalMap.Sample(uv).xyz * 2.0 - 1.0; float3 T = normalize(input.Tangent.xyz); float3 B = normalize(cross(N, T)) * input.Tangent.w; float3x3 TBN = float3x3(T, B, N); diff --git a/attachments/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang index be0b626a..e0ca5614 100644 --- a/attachments/simple_engine/shaders/physics.slang +++ b/attachments/simple_engine/shaders/physics.slang @@ -151,6 +151,7 @@ bool aabbOverlap(float3 minA, float3 maxA, float3 minB, float3 maxB) { // Broad phase collision detection - identifies potential collision pairs [shader("compute")] +[numthreads(64, 1, 1)] void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { uint index = dispatchThreadID.x; @@ -194,6 +195,13 @@ void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { return; } + // Early culling: only consider pairs where at least one body is a sphere (shape 0) + int shapeA = int(bodyA.colliderData.w); + int shapeB = int(bodyB.colliderData.w); + if (!(shapeA == 0 || shapeB == 0)) { + return; + } + // Compute AABBs float3 minA, maxA, minB, maxB; computeAABB(bodyA, minA, maxA); diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang index 9c0b0373..a32f34cc 100644 --- a/attachments/simple_engine/shaders/texturedMesh.slang +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -3,10 +3,14 @@ // Input from vertex buffer struct VSInput { - float3 Position : POSITION; - float3 Normal : NORMAL; - float2 TexCoord : TEXCOORD0; - float4 Tangent : TANGENT; + [[vk::location(0)]] float3 Position; + [[vk::location(1)]] float3 Normal; + [[vk::location(2)]] float2 TexCoord; + [[vk::location(3)]] float4 Tangent; + + // Per-instance data as true matrices; occupy locations 4..7 and 8..10 respectively + [[vk::location(4)]] column_major float4x4 InstanceModelMatrix; // binding 1 (consumes 4 locations) + [[vk::location(8)]] column_major float4x3 InstanceNormalMatrix; // binding 1 (consumes 3 locations) }; // Output from vertex shader / Input to fragment shader @@ -35,13 +39,20 @@ VSOutput VSMain(VSInput input) { VSOutput output; - // Transform position to clip space - float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + // Use instance matrices directly (CPU uploads column-major matrices in attributes 4..10) + float4x4 instanceModelMatrix = input.InstanceModelMatrix; + float3x3 normalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; + + // Transform position to world space: entity model * instance model + float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); + + // Final clip space position output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); - // Pass world position and transformed normal to fragment shader + // Pass world position and transformed normal to fragment shader (apply entity model to normals too) + float3x3 model3x3 = (float3x3)ubo.model; output.WorldPos = worldPos.xyz; - output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + output.Normal = normalize(mul(model3x3, mul(normalMatrix3x3, input.Normal))); output.TexCoord = input.TexCoord; output.Tangent = input.Tangent; // Pass through tangent (unused in basic rendering) @@ -52,24 +63,20 @@ VSOutput VSMain(VSInput input) [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET { - // Sample the texture - float4 texColor = texSampler.Sample(input.TexCoord); + // Sample the texture with flipped V coordinate (glTF UV origin vs Vulkan) + float2 uv = float2(input.TexCoord.x, 1.0 - input.TexCoord.y); + float4 texColor = texSampler.Sample(uv); // Simple directional lighting float3 lightDir = normalize(float3(0.5, 1.0, 0.3)); // Fixed light direction float3 normal = normalize(input.Normal); float lightIntensity = max(dot(normal, lightDir), 0.2); // Minimum ambient of 0.2 - // Check if texture is pure white (indicates no meaningful texture data) + // If texture is nearly white, use a default color to avoid washed-out look float whiteness = (texColor.r + texColor.g + texColor.b) / 3.0; - bool isPureWhite = whiteness > 0.95; // Threshold for "pure white" + float4 finalColor = (whiteness > 0.95) + ? float4(float3(0.8, 0.8, 0.8) * lightIntensity, 1.0) + : float4(texColor.rgb * lightIntensity, texColor.a); - if (isPureWhite) { - // No texture or pure white texture: use a default color with lighting - float3 defaultColor = float3(0.8, 0.8, 0.8); // Light gray - return float4(defaultColor * lightIntensity, 1.0); - } else { - // Apply simple lighting to texture - return float4(texColor.rgb * lightIntensity, texColor.a); - } + return finalColor; } diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc index 9104295d..bb187795 100644 --- a/en/Building_a_Simple_Engine/introduction.adoc +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -63,3 +63,24 @@ Let's begin our journey into engine development with these chapters: 8. link:Mobile_Development/01_introduction.adoc[Mobile Development] - Adapting the engine for Android/iOS, focusing on performance considerations and mobile-specific Vulkan extensions. link:../conclusion.adoc[Previous: Main Tutorial Conclusion] | link:Engine_Architecture/01_introduction.adoc[Next: Engine Architecture] + + +=== Getting Started with Example Assets + +To follow along with the attachments-based Simple Engine examples and scenes, fetch the Bistro assets locally. + +- Linux/macOS (default target: attachments/simple_engine/Assets/bistro at repository root): ++ + $ cd attachments/simple_engine + $ ./fetch_bistro_assets.sh + +- Windows (default target: attachments\simple_engine\Assets\bistro at repository root): ++ + > cd attachments\simple_engine + > fetch_bistro_assets.bat + +The scripts use SSH (git@github.com:gpx1000/bistro.git) and fall back to HTTPS if SSH is unavailable. If Git LFS is installed, large files will be pulled automatically. + +Next, take advantage of the install_dependencies_* scripts to ensure you have all necessary dependencies. + +Once assets are available and dependencies are ready, build and run the Simple Engine examples under attachments/simple_engine. See the later chapters for details on scene loading and subsystems referenced by the example code. From dd9713a3b032a324d6b209ae1e75a05c9941a7a1 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 10 Aug 2025 09:27:11 -0700 Subject: [PATCH 046/102] Refactor Vulkan memory handling, physics GPU initialization, and collision detection - Improved memory mapping reliability by enforcing VK_WHOLE_SIZE flush alignment. - Transitioned to quaternions for stable rotation in transformation updates. - Adjust bounciness to make more interesting. - Enhanced GPU-only physics setup with better Vulkan resource management. - Fixed semaphore usage issues in frame rendering pipeline. - Simplified texture fallback loading with assertion-based policy. --- attachments/simple_engine/engine.cpp | 4 +- .../simple_engine/fetch_bistro_assets.bat | 5 +- .../simple_engine/fetch_bistro_assets.sh | 2 +- attachments/simple_engine/model_loader.h | 11 -- attachments/simple_engine/physics_system.cpp | 120 +++++++++++++----- attachments/simple_engine/physics_system.h | 6 +- .../simple_engine/renderer_rendering.cpp | 4 +- .../simple_engine/renderer_resources.cpp | 17 +-- attachments/simple_engine/scene_loading.cpp | 4 +- .../simple_engine/shaders/physics.slang | 59 ++++++++- .../simple_engine/transform_component.cpp | 15 ++- 11 files changed, 166 insertions(+), 81 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index af8cc736..9f64fc62 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -562,8 +562,8 @@ void Engine::GenerateBallMaterial() { // Random emissive color (usually subtle) ballMaterial.emissive = glm::vec3(dis(gen) * 0.3f, dis(gen) * 0.3f, dis(gen) * 0.3f); - // Very low bounciness (0.05 to 0.15 for 85-95% momentum loss per bounce) - ballMaterial.bounciness = 0.05f + dis(gen) * 0.1f; + // Decent bounciness (0.6 to 0.9) so bounces are clearly visible + ballMaterial.bounciness = 0.6f + dis(gen) * 0.3f; } void Engine::InitializePhysicsScaling() { diff --git a/attachments/simple_engine/fetch_bistro_assets.bat b/attachments/simple_engine/fetch_bistro_assets.bat index b8411b05..57efdcd6 100644 --- a/attachments/simple_engine/fetch_bistro_assets.bat +++ b/attachments/simple_engine/fetch_bistro_assets.bat @@ -5,15 +5,14 @@ REM Fetch the Bistro example assets into the desired assets directory. REM Default target: assets\bistro at the repository root. REM Usage: REM fetch_bistro_assets.bat [target-dir] -REM Examples: +REM Example: REM fetch_bistro_assets.bat -REM fetch_bistro_assets.bat attachments\simple_engine\Assets\bistro set REPO_SSH=git@github.com:gpx1000/bistro.git set REPO_HTTPS=https://github.com/gpx1000/bistro.git if "%~1"=="" ( - set TARGET_DIR=assets\bistro + set TARGET_DIR=Assets\bistro ) else ( set TARGET_DIR=%~1 ) diff --git a/attachments/simple_engine/fetch_bistro_assets.sh b/attachments/simple_engine/fetch_bistro_assets.sh index 9580149c..98fc829b 100755 --- a/attachments/simple_engine/fetch_bistro_assets.sh +++ b/attachments/simple_engine/fetch_bistro_assets.sh @@ -10,7 +10,7 @@ set -euo pipefail REPO_SSH="git@github.com:gpx1000/bistro.git" REPO_HTTPS="https://github.com/gpx1000/bistro.git" -TARGET_DIR="${1:-assets/bistro}" +TARGET_DIR="${1:-Assets/bistro}" mkdir -p "$(dirname "${TARGET_DIR}")" diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index 2fbe6eea..57bd345b 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -259,17 +259,6 @@ class ModelLoader { */ bool ParseGLTF(const std::string& filename, Model* model); - /** - * @brief Load textures for a PBR material. - * @param material The material to populate. - * @param albedoMap The path to the albedo texture. - * @param normalMap The path to the normal texture. - * @param metallicRoughnessMap The path to the metallic-roughness texture. - * @param aoMap The path to the ambient occlusion texture. - * @param emissiveMap The path to the emissive texture. - * @return True if loading was successful, false otherwise. - */ - /** * @brief Extract lights from GLTF punctual lights extension. * @param gltfModel The loaded GLTF model. diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index 1e19f2e8..04020a54 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -180,15 +180,21 @@ PhysicsSystem::~PhysicsSystem() { } bool PhysicsSystem::Initialize() { - // This is a placeholder implementation - // In a real implementation, this would initialize the physics engine - - // Initialize Vulkan resources if GPU acceleration is enabled and the renderer is set - if (gpuAccelerationEnabled && renderer) { - if (!InitializeVulkanResources()) { - std::cerr << "Failed to initialize Vulkan resources for physics system" << std::endl; - gpuAccelerationEnabled = false; - } + // Enforce GPU-only physics. If GPU resources cannot be initialized, initialization fails. + + // Renderer must be set for GPU compute physics + if (!renderer) { + std::cerr << "PhysicsSystem::Initialize: Renderer is not set. GPU-only physics cannot proceed." << std::endl; + return false; + } + + // Always keep GPU acceleration enabled (CPU fallback is not allowed) + gpuAccelerationEnabled = true; + + // Initialize Vulkan resources; fail hard if not available + if (!InitializeVulkanResources()) { + std::cerr << "PhysicsSystem::Initialize: Failed to initialize Vulkan resources for physics (GPU-only)." << std::endl; + return false; } initialized = true; @@ -292,7 +298,7 @@ bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, switch (shape) { case CollisionShape::Sphere: { // Sphere intersection test - float radius = 0.5f; // Default radius + float radius = 0.0335f; // Tennis ball radius to match actual ball // Calculate coefficients for quadratic equation glm::vec3 oc = origin - position; @@ -765,14 +771,10 @@ bool PhysicsSystem::InitializeVulkanResources() { // Create persistent mapped memory pointers for improved performance try { - // Map physics buffer memory persistently - vulkanResources.persistentPhysicsMemory = vulkanResources.physicsBufferMemory.mapMemory(0, physicsBufferSize); - - // Map counter buffer memory persistently - vulkanResources.persistentCounterMemory = vulkanResources.counterBufferMemory.mapMemory(0, counterBufferSize); - - // Map params buffer memory persistently - vulkanResources.persistentParamsMemory = vulkanResources.paramsBufferMemory.mapMemory(0, paramsBufferSize); + // Map entire memory objects persistently to satisfy VK_WHOLE_SIZE flush alignment requirements + vulkanResources.persistentPhysicsMemory = vulkanResources.physicsBufferMemory.mapMemory(0, VK_WHOLE_SIZE); + vulkanResources.persistentCounterMemory = vulkanResources.counterBufferMemory.mapMemory(0, VK_WHOLE_SIZE); + vulkanResources.persistentParamsMemory = vulkanResources.paramsBufferMemory.mapMemory(0, VK_WHOLE_SIZE); } catch (const std::exception& e) { throw std::runtime_error("Failed to create persistent mapped memory: " + std::string(e.what())); } @@ -877,10 +879,10 @@ bool PhysicsSystem::InitializeVulkanResources() { raiiDevice.updateDescriptorSets(descriptorWrites, nullptr); - // Create a command pool + // Create a command pool bound to the compute queue family used by the renderer vk::CommandPoolCreateInfo commandPoolInfo; commandPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; - commandPoolInfo.queueFamilyIndex = 0; // Assuming the compute queue family index is 0 + commandPoolInfo.queueFamilyIndex = renderer->GetComputeQueueFamilyIndex(); vulkanResources.commandPool = vk::raii::CommandPool(raiiDevice, commandPoolInfo); // Allocate command buffer @@ -1009,8 +1011,10 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { // For dynamic bodies (balls), allow forces to be applied by // The shader will add gravity and other forces each frame - gpuData[i].force = glm::vec4(initialForce, concreteRigidBody->IsKinematic() ? 1.0f : 0.0f); - gpuData[i].torque = glm::vec4(initialTorque, 1.0f); // Always use gravity + bool isKinematic = concreteRigidBody->IsKinematic(); + gpuData[i].force = glm::vec4(initialForce, isKinematic ? 1.0f : 0.0f); + // Use gravity only for dynamic bodies + gpuData[i].torque = glm::vec4(initialTorque, isKinematic ? 0.0f : 1.0f); // Set collider data based on a collider type switch (concreteRigidBody->GetShape()) { @@ -1040,7 +1044,9 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { glm::vec3 localCenter = 0.5f * (localMin + localMax); glm::vec3 localHalfExtents = 0.5f * (localMax - localMin); - glm::mat4 model = xform->GetModelMatrix(); + glm::mat4 model = (meshComp->GetInstanceCount() > 0) + ? meshComp->GetInstance(0).getModelMatrix() + : xform->GetModelMatrix(); glm::vec3 centerWS = glm::vec3(model * glm::vec4(localCenter, 1.0f)); glm::mat3 RS = glm::mat3(model); @@ -1057,7 +1063,8 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { } } - gpuData[i].colliderData = glm::vec4(halfExtents, static_cast(2)); // 2 = Mesh (as Box) + // Encode Mesh collider as Mesh (type=2) for GPU narrowphase handling (sphere vs mesh) + gpuData[i].colliderData = glm::vec4(halfExtents, static_cast(2)); // 2 = Mesh (represented as world AABB) gpuData[i].colliderData2 = glm::vec4(localOffset, 0.0f); } break; @@ -1089,14 +1096,26 @@ void PhysicsSystem::UpdateGPUPhysicsData(float deltaTime) const { // Use VK_WHOLE_SIZE to avoid nonCoherentAtomSize alignment validation errors try { const vk::raii::Device& device = renderer->GetRaiiDevice(); - vk::MappedMemoryRange flushRange; - flushRange.memory = *vulkanResources.paramsBufferMemory; - flushRange.offset = 0; - flushRange.size = VK_WHOLE_SIZE; - - device.flushMappedMemoryRanges(flushRange); + // Flush params buffer + vk::MappedMemoryRange flushRangeParams; + flushRangeParams.memory = *vulkanResources.paramsBufferMemory; + flushRangeParams.offset = 0; + flushRangeParams.size = VK_WHOLE_SIZE; + device.flushMappedMemoryRanges(flushRangeParams); + // Flush physics buffer (object data) + vk::MappedMemoryRange flushRangePhysics; + flushRangePhysics.memory = *vulkanResources.physicsBufferMemory; + flushRangePhysics.offset = 0; + flushRangePhysics.size = VK_WHOLE_SIZE; + device.flushMappedMemoryRanges(flushRangePhysics); + // Flush counter buffer (pair and collision counters) + vk::MappedMemoryRange flushRangeCounter; + flushRangeCounter.memory = *vulkanResources.counterBufferMemory; + flushRangeCounter.offset = 0; + flushRangeCounter.size = VK_WHOLE_SIZE; + device.flushMappedMemoryRanges(flushRangeCounter); } catch (const std::exception& e) { - fprintf(stderr, "WARNING: Failed to flush params buffer memory: %s", e.what()); + fprintf(stderr, "WARNING: Failed to flush mapped physics memory: %s", e.what()); } } @@ -1118,6 +1137,39 @@ void PhysicsSystem::ReadbackGPUPhysicsData() const { return; } + // Ensure GPU writes to HOST_VISIBLE memory are visible to the host before reading + try { + vk::MappedMemoryRange invalidateRangePhysics; + invalidateRangePhysics.memory = *vulkanResources.physicsBufferMemory; + invalidateRangePhysics.offset = 0; + invalidateRangePhysics.size = VK_WHOLE_SIZE; + + vk::MappedMemoryRange invalidateRangeCounter; + invalidateRangeCounter.memory = *vulkanResources.counterBufferMemory; + invalidateRangeCounter.offset = 0; + invalidateRangeCounter.size = VK_WHOLE_SIZE; + + device.invalidateMappedMemoryRanges({invalidateRangePhysics, invalidateRangeCounter}); + } catch (const std::exception&) { + // On HOST_COHERENT heaps this may not be required; ignore errors + } + + // Optional debug: read and log pair/collision counters for a few frames + if (vulkanResources.persistentCounterMemory) { + static uint32_t lastPairCount = UINT32_MAX; + static uint32_t lastCollisionCount = UINT32_MAX; + static int debugFrames = 0; + const uint32_t* counters = static_cast(vulkanResources.persistentCounterMemory); + uint32_t pairCount = counters[0]; + uint32_t collisionCount = counters[1]; + if (debugFrames < 120 && (pairCount != lastPairCount || collisionCount != lastCollisionCount)) { + std::cout << "Physics GPU counters - pairs: " << pairCount << ", collisions: " << collisionCount << std::endl; + lastPairCount = pairCount; + lastCollisionCount = collisionCount; + debugFrames++; + } + } + // Skip physics buffer operations if no rigid bodies exist if (!rigidBodies.empty()) { // Use persistent mapped memory for physics buffer readback @@ -1176,11 +1228,11 @@ void PhysicsSystem::SimulatePhysicsOnGPU(const float deltaTime) const { nullptr ); - // Add a memory barrier to ensure host-written uniform buffer data is visible to shader - // This ensures the PhysicsParams data uploaded from CPU is properly synchronized before shader execution + // Add a memory barrier to ensure all host-written buffer data (uniform + storage) is visible to compute shaders + // We use ShaderRead | ShaderWrite since compute will read and write storage buffers vk::MemoryBarrier hostToDeviceBarrier; hostToDeviceBarrier.srcAccessMask = vk::AccessFlagBits::eHostWrite; - hostToDeviceBarrier.dstAccessMask = vk::AccessFlagBits::eUniformRead; + hostToDeviceBarrier.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; vulkanResources.commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eHost, diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index 1ecb86d2..4c264799 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -256,7 +256,11 @@ class PhysicsSystem { * @brief Enable or disable GPU acceleration. * @param enabled Whether GPU acceleration is enabled. */ - void SetGPUAccelerationEnabled(bool enabled) { gpuAccelerationEnabled = enabled; } + void SetGPUAccelerationEnabled(bool enabled) { + // Enforce GPU-only policy: disabling GPU acceleration is not allowed in this project. + // Ignore attempts to disable and keep GPU acceleration enabled. + gpuAccelerationEnabled = true; + } /** * @brief Check if GPU acceleration is enabled. diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 065e1ce6..034c2342 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -745,7 +745,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, - .pSignalSemaphores = &*renderFinishedSemaphores[currentFrame] + .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex] }; // Use mutex to ensure thread-safe access to graphics queue @@ -757,7 +757,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam // Present the image vk::PresentInfoKHR presentInfo{ .waitSemaphoreCount = 1, - .pWaitSemaphores = &*renderFinishedSemaphores[currentFrame], + .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 71f44ff2..2aa8c742 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -160,23 +160,8 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour if (loaded) break; } } - if (result != KTX_SUCCESS) { - // Last-ditch fallback: try alternate image formats (.png/.jpg/.jpeg) with same basename - std::filesystem::path orig = std::filesystem::path(texturePath); - std::vector altExts = {".png", ".jpg", ".jpeg", ".PNG", ".JPG", ".JPEG", ".dds", ".DDS", ".ktx", ".KTX"}; - for (const auto& ext : altExts) { - std::filesystem::path cand = orig; - cand.replace_extension(ext); - if (std::filesystem::exists(cand)) { - // Alternate non-KTX fallback removed per simplification policy - (void)ext; (void)orig; (void)cand; // suppress unused warnings - } - } - std::cerr << "Failed to load KTX2 texture: " << texturePath << " (error: " << result << ")" << std::endl; - return false; - } + assert (result != KTX_SUCCESS); } - ktx2_or_alt_loaded:; // Cache header-provided VkFormat ktxHeaderVkFormat = static_cast(ktxTex->vkFormat); diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 46d59981..ebd446de 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -113,7 +113,7 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Apply rotation from GLTF camera glm::vec3 eulerAngles = glm::eulerAngles(gltfCamera.rotation); - cameraTransform->SetRotation(glm::degrees(eulerAngles)); + cameraTransform->SetRotation(eulerAngles); } // Update the camera component with GLTF properties @@ -156,7 +156,7 @@ void LoadGLTFModel(Engine* engine, const std::string& modelPath, // Add a transform component with provided parameters auto* transform = materialEntity->AddComponent(); transform->SetPosition(position); - transform->SetRotation(rotation); + transform->SetRotation(glm::radians(rotation)); transform->SetScale(scale); // Add a mesh component with material-specific data diff --git a/attachments/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang index e0ca5614..82822970 100644 --- a/attachments/simple_engine/shaders/physics.slang +++ b/attachments/simple_engine/shaders/physics.slang @@ -207,6 +207,16 @@ void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { computeAABB(bodyA, minA, maxA); computeAABB(bodyB, minB, maxB); + // Expand sphere AABBs by motion over the timestep to catch fast-moving spheres + if (shapeA == 0) { + float3 expandA = abs(bodyA.linearVelocity.xyz) * params.deltaTime; + minA -= expandA; maxA += expandA; + } + if (shapeB == 0) { + float3 expandB = abs(bodyB.linearVelocity.xyz) * params.deltaTime; + minB -= expandB; maxB += expandB; + } + // Check for AABB overlap if (aabbOverlap(minA, maxA, minB, maxB)) { // Add to potential collision pairs @@ -295,13 +305,13 @@ void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { float distance = length(direction); if (distance < sphereRadius) { - // Collision detected + // Collision detected (overlap) uint collisionIndex; InterlockedAdd(counterBuffer[1], 1, collisionIndex); if (collisionIndex < params.maxCollisions) { - // Calculate normal (from geometry to sphere) - float3 normal = (distance > 0.0001) ? direction / distance : float3(0, 1, 0); + // Calculate normal so that it points from sphere(A) to geometry(B) + float3 normal = (distance > 0.0001) ? (-direction / distance) : float3(0, -1, 0); float penetration = sphereRadius - distance; // Create collision data @@ -314,6 +324,49 @@ void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { // Store collision data collisionBuffer[collisionIndex] = collision; } + } else { + // Swept test (CCD-lite): segment from previous position to current against box expanded by sphere radius + float3 prevPos = spherePos - sphere.linearVelocity.xyz * params.deltaTime; + float3 dir = spherePos - prevPos; + float dirLen = length(dir); + if (dirLen > 1e-6) { + float3 bbMin = geometryPos - (geometryHalfExtents + sphereRadius); + float3 bbMax = geometryPos + (geometryHalfExtents + sphereRadius); + + float3 invDir = 1.0 / max(abs(dir), float3(1e-6, 1e-6, 1e-6)); + float3 t0 = (bbMin - prevPos) / dir; + float3 t1 = (bbMax - prevPos) / dir; + float3 tmin3 = min(t0, t1); + float3 tmax3 = max(t0, t1); + float tEnter = max(tmin3.x, max(tmin3.y, tmin3.z)); + float tExit = min(tmax3.x, min(tmax3.y, tmax3.z)); + + if (tEnter >= 0.0 && tEnter <= 1.0 && tEnter <= tExit) { + // Determine contact normal based on entry axis and direction of motion + float3 normal = float3(0,0,0); + if (tEnter >= tmin3.x && tEnter >= tmin3.y && tEnter >= tmin3.z) { + normal = float3((dir.x > 0.0) ? 1.0 : -1.0, 0.0, 0.0); + } else if (tEnter >= tmin3.y && tEnter >= tmin3.z) { + normal = float3(0.0, (dir.y > 0.0) ? 1.0 : -1.0, 0.0); + } else { + normal = float3(0.0, 0.0, (dir.z > 0.0) ? 1.0 : -1.0); + } + + float3 hitPoint = prevPos + dir * tEnter; + + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + if (collisionIndex < params.maxCollisions) { + CollisionData collision; + collision.bodyA = sphereIndex; + collision.bodyB = geometryIndex; + // Tiny penetration to trigger resolution without large positional correction + collision.contactNormal = float4(normalize(normal), 0.0); + collision.contactPoint = float4(hitPoint, 0.0); + collisionBuffer[collisionIndex] = collision; + } + } + } } } } diff --git a/attachments/simple_engine/transform_component.cpp b/attachments/simple_engine/transform_component.cpp index 0682d47d..b25ffae5 100644 --- a/attachments/simple_engine/transform_component.cpp +++ b/attachments/simple_engine/transform_component.cpp @@ -18,11 +18,14 @@ const glm::mat4& TransformComponent::GetModelMatrix() { // Updates the model matrix based on position, rotation, and scale // @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix void TransformComponent::UpdateModelMatrix() { - modelMatrix = glm::mat4(1.0f); - modelMatrix = glm::translate(modelMatrix, position); - modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); - modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); - modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); - modelMatrix = glm::scale(modelMatrix, scale); + // Compose rotation with quaternions for stability and to avoid rad/deg ambiguity + glm::mat4 T = glm::translate(glm::mat4(1.0f), position); + glm::quat qx = glm::angleAxis(rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); + glm::quat qy = glm::angleAxis(rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::quat qz = glm::angleAxis(rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); + glm::quat q = qz * qy * qx; // ZYX order is conventional for Euler composition + glm::mat4 R = glm::mat4_cast(q); + glm::mat4 S = glm::scale(glm::mat4(1.0f), scale); + modelMatrix = T * R * S; matrixDirty = false; } From 27715c0ac5884220b93447b508563b3de95844c1 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 10 Aug 2025 11:13:11 -0700 Subject: [PATCH 047/102] address several comments (more still need to be addressed). And fix the install_dependencies_windows.bat to be more user friendly. --- attachments/simple_engine/audio_system.cpp | 2 +- attachments/simple_engine/debug_system.h | 4 +- .../simple_engine/descriptor_manager.cpp | 114 ++++---- .../simple_engine/descriptor_manager.h | 7 +- attachments/simple_engine/engine.cpp | 196 ++++++------- attachments/simple_engine/engine.h | 26 +- attachments/simple_engine/entity.h | 2 +- attachments/simple_engine/imgui_system.cpp | 109 ++++--- .../install_dependencies_windows.bat | 271 +++++++++--------- 9 files changed, 369 insertions(+), 362 deletions(-) diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 4af3b490..df283be4 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -385,7 +385,7 @@ class OpenALAudioOutputDevice : public AudioOutputDevice { std::vector pcmBuffer(samplesProcessed); for (uint32_t i = 0; i < samplesProcessed; i++) { // Clamp and convert to 16-bit PCM - float sample = std::max(-1.0f, std::min(1.0f, audioBuffer[i])); + float sample = std::clamp( -1.0f, 1.0f, audioBuffer[i] ); pcmBuffer[i] = static_cast(sample * 32767.0f); } diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h index 2f6ae385..5856491c 100644 --- a/attachments/simple_engine/debug_system.h +++ b/attachments/simple_engine/debug_system.h @@ -193,9 +193,7 @@ class DebugSystem { * @brief End a performance measurement and log the result. * @param name The name of the measurement. */ - void EndMeasurement(const std::string& name) { - std::lock_guard lock(mutex); - + void StopMeasurement(const std::string& name) { auto now = std::chrono::high_resolution_clock::now(); auto it = measurements.find(name); diff --git a/attachments/simple_engine/descriptor_manager.cpp b/attachments/simple_engine/descriptor_manager.cpp index a8f6c6d6..bb46b794 100644 --- a/attachments/simple_engine/descriptor_manager.cpp +++ b/attachments/simple_engine/descriptor_manager.cpp @@ -1,7 +1,7 @@ #include "descriptor_manager.h" #include #include -#include +#include #include "transform_component.h" #include "camera_component.h" @@ -11,9 +11,7 @@ DescriptorManager::DescriptorManager(VulkanDevice& device) } // Destructor -DescriptorManager::~DescriptorManager() { - // RAII will handle destruction -} +DescriptorManager::~DescriptorManager() = default; // Create descriptor pool bool DescriptorManager::createDescriptorPool(uint32_t maxSets) { @@ -88,9 +86,58 @@ bool DescriptorManager::createUniformBuffers(Entity* entity, uint32_t maxFramesI } } +bool DescriptorManager::update_descriptor_sets(Entity* entity, uint32_t maxFramesInFlight, bool& value1) { + assert(entityResources[entity].uniformBuffers.size() == maxFramesInFlight); + // Update descriptor sets + for (size_t i = 0; i < maxFramesInFlight; i++) { + // Create descriptor buffer info + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *entityResources[entity].uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + // Create descriptor image info + vk::DescriptorImageInfo imageInfo{ + // These would be set based on the texture resources + // .sampler = textureSampler, + // .imageView = textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + // Create descriptor writes + std::array descriptorWrites = { + vk::WriteDescriptorSet{ + .dstSet = entityResources[entity].descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pImageInfo = nullptr, + .pBufferInfo = &bufferInfo, + .pTexelBufferView = nullptr + }, + vk::WriteDescriptorSet{ + .dstSet = entityResources[entity].descriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo, + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr + } + }; + + // Update descriptor sets + device.getDevice().updateDescriptorSets(descriptorWrites, nullptr); + } + return false; +} // Create descriptor sets for an entity bool DescriptorManager::createDescriptorSets(Entity* entity, const std::string& texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight) { try { + assert(entityResources.find(entity) != entityResources.end()); // Create descriptor sets for each frame in flight std::vector layouts(maxFramesInFlight, descriptorSetLayout); @@ -101,61 +148,10 @@ bool DescriptorManager::createDescriptorSets(Entity* entity, const std::string& .pSetLayouts = layouts.data() }; - // Allocate descriptor sets - auto descriptorSets = device.getDevice().allocateDescriptorSets(allocInfo); + entityResources[entity].descriptorSets = device.getDevice().allocateDescriptorSets(allocInfo); - // Store descriptor sets - // Convert from vk::raii::DescriptorSet to vk::DescriptorSet - std::vector nonRaiiDescriptorSets; - for (const auto& ds : descriptorSets) { - nonRaiiDescriptorSets.push_back(*ds); - } - entityResources[entity].descriptorSets = nonRaiiDescriptorSets; - - // Update descriptor sets - for (size_t i = 0; i < maxFramesInFlight; i++) { - // Create descriptor buffer info - vk::DescriptorBufferInfo bufferInfo{ - .buffer = *entityResources[entity].uniformBuffers[i], - .offset = 0, - .range = sizeof(UniformBufferObject) - }; - - // Create descriptor image info - vk::DescriptorImageInfo imageInfo{ - // These would be set based on the texture resources - // .sampler = textureSampler, - // .imageView = textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - - // Create descriptor writes - std::array descriptorWrites = { - vk::WriteDescriptorSet{ - .dstSet = descriptorSets[i], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pImageInfo = nullptr, - .pBufferInfo = &bufferInfo, - .pTexelBufferView = nullptr - }, - vk::WriteDescriptorSet{ - .dstSet = descriptorSets[i], - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = &imageInfo, - .pBufferInfo = nullptr, - .pTexelBufferView = nullptr - } - }; - - // Update descriptor sets - device.getDevice().updateDescriptorSets(descriptorWrites, nullptr); - } + bool value1; + if (update_descriptor_sets(entity, maxFramesInFlight, value1)) return value1; return true; } catch (const std::exception& e) { @@ -192,6 +188,8 @@ void DescriptorManager::updateUniformBuffer(uint32_t currentImage, Entity* entit ubo.lightPos = glm::vec4(0.0f, 5.0f, 0.0f, 1.0f); ubo.lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); + assert(entityResources.find(entity) != entityResources.end()); + assert(entityResources[entity].uniformBuffers.size() > currentImage); // Copy data to uniform buffer memcpy(entityResources[entity].uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); } diff --git a/attachments/simple_engine/descriptor_manager.h b/attachments/simple_engine/descriptor_manager.h index cd6569ec..48319b4d 100644 --- a/attachments/simple_engine/descriptor_manager.h +++ b/attachments/simple_engine/descriptor_manager.h @@ -34,7 +34,7 @@ class DescriptorManager { std::vector uniformBuffers; std::vector uniformBuffersMemory; std::vector uniformBuffersMapped; - std::vector descriptorSets; + std::vector descriptorSets; }; /** @@ -62,6 +62,7 @@ class DescriptorManager { * @return True if the uniform buffers were created successfully, false otherwise. */ bool createUniformBuffers(Entity* entity, uint32_t maxFramesInFlight); + bool update_descriptor_sets(Entity* entity, uint32_t maxFramesInFlight, bool& value1); /** * @brief Create descriptor sets for an entity. @@ -91,14 +92,14 @@ class DescriptorManager { * @brief Get the entity resources. * @return The entity resources. */ - std::unordered_map& getEntityResources() { return entityResources; } + const std::unordered_map& getEntityResources() { return entityResources; } /** * @brief Get the resources for an entity. * @param entity The entity. * @return The entity resources. */ - EntityResources& getEntityResources(Entity* entity) { return entityResources[entity]; } + const EntityResources& getEntityResources(Entity* entity) { return entityResources[entity]; } private: // Vulkan device diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 9f64fc62..f56f8ada 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -42,95 +42,12 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Set mouse callback platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { - // Check if ImGui wants to capture mouse input first - bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); - - if (!imguiWantsMouse) { - // Handle mouse click for ball throwing (right mouse button) - if (buttons & 2) { // Right mouse button (bit 1) - if (!cameraControl.mouseRightPressed) { - cameraControl.mouseRightPressed = true; - // Throw a ball on mouse click - ThrowBall(x, y); - } - } else { - cameraControl.mouseRightPressed = false; - } - - // Handle camera rotation when left mouse button is pressed - if (buttons & 1) { // Left mouse button (bit 0) - if (!cameraControl.mouseLeftPressed) { - cameraControl.mouseLeftPressed = true; - cameraControl.firstMouse = true; - } - - if (cameraControl.firstMouse) { - cameraControl.lastMouseX = x; - cameraControl.lastMouseY = y; - cameraControl.firstMouse = false; - } - - float xOffset = x - cameraControl.lastMouseX; - float yOffset = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top - cameraControl.lastMouseX = x; - cameraControl.lastMouseY = y; - - xOffset *= cameraControl.mouseSensitivity; - yOffset *= cameraControl.mouseSensitivity; - - cameraControl.yaw += xOffset; - cameraControl.pitch += yOffset; - - // Constrain pitch to avoid gimbal lock - if (cameraControl.pitch > 89.0f) cameraControl.pitch = 89.0f; - if (cameraControl.pitch < -89.0f) cameraControl.pitch = -89.0f; - } else { - cameraControl.mouseLeftPressed = false; - } - } - - if (imguiSystem) { - imguiSystem->HandleMouse(x, y, buttons); - } - - // Always perform hover detection (even when ImGui is active) - HandleMouseHover(x, y); + handleMouseInput(x, y, buttons); }); // Set keyboard callback platform->SetKeyboardCallback([this](uint32_t key, bool pressed) { - // Handle camera movement keys (WASD + Arrow keys) - switch (key) { - case GLFW_KEY_W: - case GLFW_KEY_UP: - cameraControl.moveForward = pressed; - break; - case GLFW_KEY_S: - case GLFW_KEY_DOWN: - cameraControl.moveBackward = pressed; - break; - case GLFW_KEY_A: - case GLFW_KEY_LEFT: - cameraControl.moveLeft = pressed; - break; - case GLFW_KEY_D: - case GLFW_KEY_RIGHT: - cameraControl.moveRight = pressed; - break; - case GLFW_KEY_Q: - case GLFW_KEY_PAGE_UP: - cameraControl.moveUp = pressed; - break; - case GLFW_KEY_E: - case GLFW_KEY_PAGE_DOWN: - cameraControl.moveDown = pressed; - break; - default: break; - } - - if (imguiSystem) { - imguiSystem->HandleKeyboard(key, pressed); - } + handleKeyInput(key, pressed); }); // Set char callback @@ -276,7 +193,7 @@ Entity* Engine::CreateEntity(const std::string& name) { return entities.back().get(); } -Entity* Engine::GetEntity(const std::string& name) { +Entity* Engine::GetEntity(const std::string& name) const { auto it = entityMap.find(name); if (it != entityMap.end()) { return it->second; @@ -363,6 +280,97 @@ ImGuiSystem* Engine::GetImGuiSystem() const { return imguiSystem.get(); } + + +void Engine::handleMouseInput(float x, float y, uint32_t buttons) { + // Check if ImGui wants to capture mouse input first + bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + + if (!imguiWantsMouse) { + // Handle mouse click for ball throwing (right mouse button) + if (buttons & 2) { // Right mouse button (bit 1) + if (!cameraControl.mouseRightPressed) { + cameraControl.mouseRightPressed = true; + // Throw a ball on mouse click + ThrowBall(x, y); + } + } else { + cameraControl.mouseRightPressed = false; + } + + // Handle camera rotation when left mouse button is pressed + if (buttons & 1) { // Left mouse button (bit 0) + if (!cameraControl.mouseLeftPressed) { + cameraControl.mouseLeftPressed = true; + cameraControl.firstMouse = true; + } + + if (cameraControl.firstMouse) { + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + cameraControl.firstMouse = false; + } + + float xOffset = x - cameraControl.lastMouseX; + float yOffset = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + + xOffset *= cameraControl.mouseSensitivity; + yOffset *= cameraControl.mouseSensitivity; + + cameraControl.yaw += xOffset; + cameraControl.pitch += yOffset; + + // Constrain pitch to avoid gimbal lock + if (cameraControl.pitch > 89.0f) cameraControl.pitch = 89.0f; + if (cameraControl.pitch < -89.0f) cameraControl.pitch = -89.0f; + } else { + cameraControl.mouseLeftPressed = false; + } + } + + if (imguiSystem) { + imguiSystem->HandleMouse(x, y, buttons); + } + + // Always perform hover detection (even when ImGui is active) + HandleMouseHover(x, y); +} +void Engine::handleKeyInput(uint32_t key, bool pressed) { + switch (key) { + case GLFW_KEY_W: + case GLFW_KEY_UP: + cameraControl.moveForward = pressed; + break; + case GLFW_KEY_S: + case GLFW_KEY_DOWN: + cameraControl.moveBackward = pressed; + break; + case GLFW_KEY_A: + case GLFW_KEY_LEFT: + cameraControl.moveLeft = pressed; + break; + case GLFW_KEY_D: + case GLFW_KEY_RIGHT: + cameraControl.moveRight = pressed; + break; + case GLFW_KEY_Q: + case GLFW_KEY_PAGE_UP: + cameraControl.moveUp = pressed; + break; + case GLFW_KEY_E: + case GLFW_KEY_PAGE_DOWN: + cameraControl.moveDown = pressed; + break; + default: break; + } + + if (imguiSystem) { + imguiSystem->HandleKeyboard(key, pressed); + } +} + void Engine::Update(float deltaTime) { // Debug: Verify Update method is being called static int updateCallCount = 0; @@ -441,6 +449,9 @@ float Engine::CalculateDeltaTime() { } void Engine::HandleResize(int width, int height) const { + if (height <= 0 || width <= 0) { + return; + } // Update the active camera's aspect ratio if (activeCamera) { activeCamera->SetAspectRatio(static_cast(width) / static_cast(height)); @@ -467,13 +478,8 @@ void Engine::UpdateCameraControls(float deltaTime) const { // Check if camera tracking is enabled if (imguiSystem && imguiSystem->IsCameraTrackingEnabled()) { // Find the first active ball entity - Entity* ballEntity = nullptr; - for (const auto& entity : entities) { - if (entity->IsActive() && entity->GetName().find("Ball_") != std::string::npos) { - ballEntity = entity.get(); - break; - } - } + auto ballEntityIt = std::ranges::find_if( entities, []( auto const & entity ){ return entity->IsActive() && ( entity->GetName().find( "Ball_" ) != std::string::npos ); } ); + Entity* ballEntity = ballEntityIt != entities.end() ? ballEntityIt->get() : nullptr; if (ballEntity) { // Get ball's transform component @@ -683,10 +689,6 @@ void Engine::ThrowBall(float mouseX, float mouseY) { } void Engine::ProcessPendingBalls() { - if (pendingBalls.empty()) { - return; - } - // Process all pending balls for (const auto& pendingBall : pendingBalls) { // Create ball entity diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 8d136ca3..71537ea9 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -65,7 +65,7 @@ class Engine { * @param name The name of the entity. * @return A pointer to the entity, or nullptr if not found. */ - Entity* GetEntity(const std::string& name); + Entity* GetEntity(const std::string& name) const; /** * @brief Remove an entity. @@ -141,6 +141,30 @@ class Engine { */ ImGuiSystem* GetImGuiSystem() const; + /** + * @brief Handles mouse input for interaction and camera control. + * + * This method processes mouse input for various functionalities, including interacting with the scene, + * camera rotation, and delegating handling to ImGui or hover systems. + * + * @param x The x-coordinate of the mouse position. + * @param y The y-coordinate of the mouse position. + * @param buttons A bitmask representing the state of mouse buttons. + * Bit 0 corresponds to the left button, and Bit 1 corresponds to the right button. + */ + void handleMouseInput(float x, float y, uint32_t buttons); + + /** + * @brief Handles keyboard input events for controlling the camera and other subsystems. + * + * This method processes key press and release events to update the camera's movement state. + * It also forwards the input to other subsystems like the ImGui interface if applicable. + * + * @param key The key code of the keyboard input. + * @param pressed Indicates whether the key is pressed (true) or released (false). + */ + void handleKeyInput(uint32_t key, bool pressed); + #if defined(PLATFORM_ANDROID) /** * @brief Initialize the engine for Android. diff --git a/attachments/simple_engine/entity.h b/attachments/simple_engine/entity.h index 2911298c..9be71f3b 100644 --- a/attachments/simple_engine/entity.h +++ b/attachments/simple_engine/entity.h @@ -155,6 +155,6 @@ class Entity { template bool HasComponent() const { static_assert(std::is_base_of::value, "T must derive from Component"); - return componentMap.find(std::type_index(typeid(T))) != componentMap.end(); + return componentMap.contains(std::type_index(typeid(T))); } }; diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index ffda91ff..b455e9b0 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -128,59 +128,60 @@ void ImGuiSystem::NewFrame() { ImGui::Text("All models rendered with basic Phong shading"); } - // BRDF Quality Controls - always available since BRDF is now default - ImGui::Separator(); - ImGui::Text("BRDF Quality Controls:"); - - // Gamma correction slider - static float gamma = 2.2f; - if (ImGui::SliderFloat("Gamma Correction", &gamma, 1.0f, 3.0f, "%.2f")) { - // Update gamma in renderer - if (renderer) { - renderer->SetGamma(gamma); + if (pbrEnabled) { + // BRDF Quality Controls + ImGui::Separator(); + ImGui::Text("BRDF Quality Controls:"); + + // Gamma correction slider + static float gamma = 2.2f; + if (ImGui::SliderFloat("Gamma Correction", &gamma, 1.0f, 3.0f, "%.2f")) { + // Update gamma in renderer + if (renderer) { + renderer->SetGamma(gamma); + } + std::cout << "Gamma set to: " << gamma << std::endl; } - std::cout << "Gamma set to: " << gamma << std::endl; - } - ImGui::SameLine(); - if (ImGui::Button("Reset##Gamma")) { - gamma = 2.2f; - if (renderer) { - renderer->SetGamma(gamma); + ImGui::SameLine(); + if (ImGui::Button("Reset##Gamma")) { + gamma = 2.2f; + if (renderer) { + renderer->SetGamma(gamma); + } + std::cout << "Gamma reset to: " << gamma << std::endl; } - std::cout << "Gamma reset to: " << gamma << std::endl; - } - // Exposure slider - static float exposure = 3.0f; // Higher default for emissive lighting - if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 10.0f, "%.2f")) { - // Update exposure in renderer - if (renderer) { - renderer->SetExposure(exposure); + // Exposure slider + static float exposure = 3.0f; // Higher default for emissive lighting + if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 10.0f, "%.2f")) { + // Update exposure in renderer + if (renderer) { + renderer->SetExposure(exposure); + } + std::cout << "Exposure set to: " << exposure << std::endl; } - std::cout << "Exposure set to: " << exposure << std::endl; - } - ImGui::SameLine(); - if (ImGui::Button("Reset##Exposure")) { - exposure = 3.0f; // Reset to higher value for emissive lighting - if (renderer) { - renderer->SetExposure(exposure); + ImGui::SameLine(); + if (ImGui::Button("Reset##Exposure")) { + exposure = 3.0f; // Reset to higher value for emissive lighting + if (renderer) { + renderer->SetExposure(exposure); + } + std::cout << "Exposure reset to: " << exposure << std::endl; } - std::cout << "Exposure reset to: " << exposure << std::endl; - } - // Shadow toggle - static bool shadowsEnabled = true; // Default shadows on - if (ImGui::Checkbox("Enable Shadows", &shadowsEnabled)) { - // Update shadows in renderer - if (renderer) { - renderer->SetShadowsEnabled(shadowsEnabled); + // Shadow toggle + static bool shadowsEnabled = true; // Default shadows on + if (ImGui::Checkbox("Enable Shadows", &shadowsEnabled)) { + // Update shadows in renderer + if (renderer) { + renderer->SetShadowsEnabled(shadowsEnabled); + } + std::cout << "Shadows " << (shadowsEnabled ? "enabled" : "disabled") << std::endl; } - std::cout << "Shadows " << (shadowsEnabled ? "enabled" : "disabled") << std::endl; - } - ImGui::Text("Tip: Adjust gamma if scene looks too dark/bright"); - ImGui::Text("Tip: Adjust exposure if scene looks washed out"); - if (!pbrEnabled) { + ImGui::Text("Tip: Adjust gamma if scene looks too dark/bright"); + ImGui::Text("Tip: Adjust exposure if scene looks washed out"); + } else { ImGui::Text("Note: Quality controls affect BRDF rendering only"); } @@ -395,13 +396,12 @@ void ImGuiSystem::Render(vk::raii::CommandBuffer & commandBuffer) { struct PushConstBlock { float scale[2]; float translate[2]; - }; - std::array pushConstBlock; + } pushConstBlock{}; - pushConstBlock[0].scale[0] = 2.0f / ImGui::GetIO().DisplaySize.x; - pushConstBlock[0].scale[1] = 2.0f / ImGui::GetIO().DisplaySize.y; - pushConstBlock[0].translate[0] = -1.0f; - pushConstBlock[0].translate[1] = -1.0f; + pushConstBlock.scale[0] = 2.0f / ImGui::GetIO().DisplaySize.x; + pushConstBlock.scale[1] = 2.0f / ImGui::GetIO().DisplaySize.y; + pushConstBlock.translate[0] = -1.0f; + pushConstBlock.translate[1] = -1.0f; commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, pushConstBlock); @@ -762,18 +762,17 @@ bool ImGuiSystem::createPipelineLayout() { bool ImGuiSystem::createPipeline() { try { // Load shaders - vk::raii::ShaderModule vertShaderModule = renderer->CreateShaderModule("shaders/imgui.spv"); - vk::raii::ShaderModule fragShaderModule = renderer->CreateShaderModule("shaders/imgui.spv"); + vk::raii::ShaderModule shaderModule = renderer->CreateShaderModule("shaders/imgui.spv"); // Shader stage creation vk::PipelineShaderStageCreateInfo vertShaderStageInfo; vertShaderStageInfo.stage = vk::ShaderStageFlagBits::eVertex; - vertShaderStageInfo.module = *vertShaderModule; + vertShaderStageInfo.module = *shaderModule; vertShaderStageInfo.pName = "VSMain"; vk::PipelineShaderStageCreateInfo fragShaderStageInfo; fragShaderStageInfo.stage = vk::ShaderStageFlagBits::eFragment; - fragShaderStageInfo.module = *fragShaderModule; + fragShaderStageInfo.module = *shaderModule; fragShaderStageInfo.pName = "PSMain"; std::array shaderStages = {vertShaderStageInfo, fragShaderStageInfo}; diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index b1d67e5c..f1279a6b 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -5,183 +5,168 @@ REM This script installs all required dependencies for building the Simple Game echo Installing Simple Game Engine dependencies for Windows... REM Check if running as administrator +REM Administrator privileges are not required. Proceeding without elevation. net session >nul 2>&1 if %errorLevel% == 0 ( - echo Running as administrator - good! + echo Running as administrator (optional). ) else ( - echo This script requires administrator privileges. - echo Please run as administrator. - pause - exit /b 1 + echo Running without administrator privileges. ) -REM Check if vcpkg is installed +REM vcpkg detection and optional local install +set "VCPKG_EXE=" where vcpkg >nul 2>&1 -if %errorLevel% == 0 ( +if %errorlevel%==0 ( echo vcpkg found in PATH + set "VCPKG_EXE=vcpkg" ) else ( - echo vcpkg not found in PATH. Installing vcpkg... - - REM Install vcpkg - if not exist "C:\vcpkg" ( - echo Cloning vcpkg to C:\vcpkg... - git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg - if %errorLevel% neq 0 ( - echo Failed to clone vcpkg. Please install Git first. - pause - exit /b 1 + echo vcpkg not found in PATH. + set "VCPKG_HOME=%USERPROFILE%\vcpkg" + set /p INSTALL_VCPKG="Install vcpkg locally to %VCPKG_HOME%? (Y/N): " + if /I "%INSTALL_VCPKG%"=="Y" ( + if not exist "%VCPKG_HOME%" ( + echo Cloning vcpkg into %VCPKG_HOME% ... + git clone https://github.com/Microsoft/vcpkg.git "%VCPKG_HOME%" + if %errorlevel% neq 0 ( + echo Failed to clone vcpkg. Ensure Git is installed and try again. + goto AFTER_VCPKG + ) + ) else ( + echo vcpkg directory already exists at %VCPKG_HOME% ) - ) - - REM Bootstrap vcpkg - echo Bootstrapping vcpkg... - cd /d C:\vcpkg - call bootstrap-vcpkg.bat - if %errorLevel% neq 0 ( - echo Failed to bootstrap vcpkg. - pause - exit /b 1 - ) - - REM Add vcpkg to PATH for this session - set PATH=%PATH%;C:\vcpkg - - REM Integrate vcpkg with Visual Studio - echo Integrating vcpkg with Visual Studio... - vcpkg integrate install -) - -REM Check if Chocolatey is installed for additional packages -where choco >nul 2>&1 -if %errorLevel% neq 0 ( - echo Installing Chocolatey package manager... - powershell -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" - if %errorLevel% neq 0 ( - echo Failed to install Chocolatey. Some dependencies may need manual installation. + pushd "%VCPKG_HOME%" + call bootstrap-vcpkg.bat + if %errorlevel% neq 0 ( + echo Failed to bootstrap vcpkg. + popd + goto AFTER_VCPKG + ) + popd + set "VCPKG_EXE=%VCPKG_HOME%\vcpkg.exe" + set /p ADD_VCPKG_PATH="Add vcpkg to PATH for this session? (Y/N): " + if /I "%ADD_VCPKG_PATH%"=="Y" set "PATH=%PATH%;%VCPKG_HOME%" + ) else ( + echo Skipping vcpkg installation. ) ) +:AFTER_VCPKG -set "CHOCO_EXE=choco" -if exist "%ProgramData%\chocolatey\bin\choco.exe" set "CHOCO_EXE=%ProgramData%\chocolatey\bin\choco.exe" - -REM Install CMake if not present +REM Tool checks (no forced install) where cmake >nul 2>&1 -if %errorLevel% neq 0 ( - echo Installing CMake... - "%CHOCO_EXE%" install cmake -y - if %errorLevel% neq 0 ( - echo Failed to install CMake via Chocolatey. Please install manually from https://cmake.org/download/ - ) +if %errorlevel%==0 ( + echo CMake found in PATH +) else ( + echo CMake not found in PATH. + set /p OPEN_CMAKE="Open CMake download page in browser? (Y/N): " + if /I "%OPEN_CMAKE%"=="Y" start "" "https://cmake.org/download/" ) -REM Install Git if not present where git >nul 2>&1 -if %errorLevel% neq 0 ( - echo Installing Git... - "%CHOCO_EXE%" install git -y - if %errorLevel% neq 0 ( - echo Failed to install Git via Chocolatey. Please install manually from https://git-scm.com/download/win - ) -) - -REM Install Vulkan SDK -echo Installing Vulkan SDK... -if not exist "C:\VulkanSDK" ( - echo Downloading and installing Vulkan SDK... - "%CHOCO_EXE%" install vulkan-sdk -y - if %errorLevel% neq 0 ( - echo Failed to install Vulkan SDK via Chocolatey. - echo Please download and install manually from https://vulkan.lunarg.com/sdk/home#windows - echo Make sure to set the VULKAN_SDK environment variable. - ) +if %errorlevel%==0 ( + echo Git found in PATH ) else ( - echo Vulkan SDK appears to be already installed. + echo Git not found in PATH. + set /p OPEN_GIT="Open Git for Windows download page? (Y/N): " + if /I "%OPEN_GIT%"=="Y" start "" "https://git-scm.com/download/win" ) -REM Install vcpkg packages -echo Installing dependencies via vcpkg... - -REM Set vcpkg triplet for x64 Windows -set VCPKG_DEFAULT_TRIPLET=x64-windows - -REM Install GLFW -echo Installing GLFW... -vcpkg install glfw3:x64-windows -if %errorLevel% neq 0 ( - echo Warning: Failed to install GLFW via vcpkg -) - -REM Install GLM -echo Installing GLM... -vcpkg install glm:x64-windows -if %errorLevel% neq 0 ( - echo Warning: Failed to install GLM via vcpkg -) - -REM Install OpenAL -echo Installing OpenAL... -vcpkg install openal-soft:x64-windows -if %errorLevel% neq 0 ( - echo Warning: Failed to install OpenAL via vcpkg +REM Vulkan SDK detection (no forced install) +set "HAVE_VULKAN_SDK=" +if defined VULKAN_SDK set "HAVE_VULKAN_SDK=1" +where vulkaninfo >nul 2>&1 +if %errorlevel%==0 set "HAVE_VULKAN_SDK=1" +if defined HAVE_VULKAN_SDK ( + echo Vulkan SDK detected. +) else ( + echo Vulkan SDK not detected. + set /p OPEN_VULKAN="Open Vulkan SDK download page (LunarG) in browser? (Y/N): " + if /I "%OPEN_VULKAN%"=="Y" start "" "https://vulkan.lunarg.com/sdk/home#windows" ) -REM Install KTX -echo Installing KTX... -vcpkg install ktx:x64-windows -if %errorLevel% neq 0 ( - echo Warning: Failed to install KTX via vcpkg +REM Optional vcpkg package installation +if defined VCPKG_EXE ( + set /p INSTALL_VCPKG_PKGS="Install common dependencies via vcpkg (glfw3, glm, openal-soft, ktx, tinygltf)? (Y/N): " + if /I "%INSTALL_VCPKG_PKGS%"=="Y" ( + set "VCPKG_DEFAULT_TRIPLET=x64-windows" + echo Installing packages with %VCPKG_EXE% (triplet %VCPKG_DEFAULT_TRIPLET%) ... + "%VCPKG_EXE%" install glfw3:%VCPKG_DEFAULT_TRIPLET% glm:%VCPKG_DEFAULT_TRIPLET% openal-soft:%VCPKG_DEFAULT_TRIPLET% ktx:%VCPKG_DEFAULT_TRIPLET% tinygltf:%VCPKG_DEFAULT_TRIPLET% + if %errorlevel% neq 0 ( + echo Warning: Some vcpkg installations may have failed. Please review output. + ) + ) else ( + echo Skipping vcpkg package installation. + ) +) else ( + echo vcpkg not available; skipping vcpkg package installation. ) -REM Install tinygltf -echo Installing tinygltf... -vcpkg install tinygltf:x64-windows -if %errorLevel% neq 0 ( - echo Warning: Failed to install tinygltf via vcpkg +REM Slang compiler detection and optional install +set "SLANGC_EXE=" +where slangc >nul 2>&1 +if %errorlevel%==0 ( + echo Slang compiler found in PATH + set "SLANGC_EXE=slangc" +) else ( + if defined VULKAN_SDK ( + if exist "%VULKAN_SDK%\Bin\slangc.exe" set "SLANGC_EXE=%VULKAN_SDK%\Bin\slangc.exe" + if not defined SLANGC_EXE if exist "%VULKAN_SDK%\Bin64\slangc.exe" set "SLANGC_EXE=%VULKAN_SDK%\Bin64\slangc.exe" + ) ) -REM Install Slang compiler -echo Installing Slang compiler... -if not exist "C:\Program Files\Slang" ( - echo Downloading Slang compiler... - set SLANG_VERSION=2024.1.21 - powershell -Command "Invoke-WebRequest -Uri 'https://github.com/shader-slang/slang/releases/download/v%SLANG_VERSION%/slang-%SLANG_VERSION%-win64.zip' -OutFile 'slang-win64.zip'" - if %errorLevel% == 0 ( - echo Extracting Slang compiler... - powershell -Command "Expand-Archive -Path 'slang-win64.zip' -DestinationPath 'C:\Program Files\Slang' -Force" - del slang-win64.zip - - REM Add Slang to PATH (requires restart or new command prompt) - echo Adding Slang to system PATH... - powershell -NoProfile -ExecutionPolicy Bypass -Command "$p=[Environment]::GetEnvironmentVariable('Path','Machine'); if(-not ($p -split ';' | ForEach-Object { $_.ToLower() }) -contains 'c:\\program files\\slang\\bin'){ [Environment]::SetEnvironmentVariable('Path',($p + ';C:\\Program Files\\Slang\\bin'),'Machine'); Write-Host 'Added Slang to machine PATH'; } else { Write-Host 'Slang already in machine PATH'; }" - echo Note: You may need to restart your command prompt for Slang to be available in PATH +if defined SLANGC_EXE ( + echo Using Slang at %SLANGC_EXE% +) else ( + echo Slang compiler (slangc) not found. + set /p INSTALL_SLANG="Download and install latest Slang locally (no admin)? (Y/N): " + if /I "%INSTALL_SLANG%"=="Y" ( + set "SLANG_ROOT=%LOCALAPPDATA%\slang" + if not exist "%SLANG_ROOT%" mkdir "%SLANG_ROOT%" + echo Downloading latest Slang release... + powershell -NoProfile -ExecutionPolicy Bypass -Command "\ +$ErrorActionPreference='Stop'; \ +$r=Invoke-RestMethod 'https://api.github.com/repos/shader-slang/slang/releases/latest'; \ +$asset=$r.assets | Where-Object { $_.name -match 'win64.*\\.zip$' } | Select-Object -First 1; \ +if(-not $asset){ throw 'No win64 asset found'; } \ +$out=Join-Path $env:TEMP $asset.name; \ +Invoke-WebRequest $asset.browser_download_url -OutFile $out; \ +Expand-Archive -Path $out -DestinationPath $env:LOCALAPPDATA\slang -Force; \ +Write-Host ('Downloaded Slang ' + $r.tag_name) \ +" + echo Locating slangc.exe... + set "SLANGC_PATH=" + for /f "delims=" %%F in ('dir /b /s "%LOCALAPPDATA%\slang\slangc.exe" 2^>nul') do ( + set "SLANGC_PATH=%%F" + goto FOUND_SLANG + ) + :FOUND_SLANG + if defined SLANGC_PATH ( + echo Found slangc at "%SLANGC_PATH%" + for %%D in ("%SLANGC_PATH%") do set "SLANG_BIN=%%~dpD" + set /p ADD_SLANG_PATH="Add Slang to PATH for this session? (Y/N): " + if /I "%ADD_SLANG_PATH%"=="Y" set "PATH=%SLANG_BIN%;%PATH%" + set "SLANGC_EXE=%SLANGC_PATH%" + ) else ( + echo Failed to locate slangc after extraction. Please install manually if needed: https://github.com/shader-slang/slang/releases + ) ) else ( - echo Failed to download Slang compiler. Please install manually from: - echo https://github.com/shader-slang/slang/releases + echo Skipping Slang installation. ) -) else ( - echo Slang compiler appears to be already installed. ) -REM Set environment variables for CMake to find vcpkg -echo Setting up CMake integration... -setx CMAKE_TOOLCHAIN_FILE "C:\vcpkg\scripts\buildsystems\vcpkg.cmake" /M -setx VCPKG_TARGET_TRIPLET "x64-windows" /M - +REM Final guidance (no machine-wide env changes) echo. -echo Dependencies installation completed! +echo Dependencies check completed! echo. -echo To build the Simple Game Engine: -echo 1. Open a new command prompt (to get updated PATH) -echo 2. cd to the simple_engine directory +echo Build instructions: +echo 1. Open a new command prompt (if you added tools to PATH for this session). +echo 2. cd to attachments\simple_engine echo 3. mkdir build ^&^& cd build -echo 4. cmake .. -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake +echo 4. If using vcpkg toolchain, run: +echo cmake .. -DCMAKE_TOOLCHAIN_FILE=%VCPKG_HOME%\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows +echo (adjust path if you installed vcpkg elsewhere; or omit this flag if not using vcpkg) echo 5. cmake --build . --config Release echo. -echo Or use Visual Studio: -echo 1. Open the CMakeLists.txt file in Visual Studio -echo 2. Visual Studio should automatically detect vcpkg integration -echo 3. Build the project using Ctrl+Shift+B +echo Alternatively open CMakeLists.txt in Visual Studio and configure normally. echo. -echo Note: You may need to restart your command prompt or IDE for environment variables to take effect. pause From 067ea7429c474b22e3cefc6c976aaadccc1f4bb1 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 10 Aug 2025 22:39:29 -0700 Subject: [PATCH 048/102] Move Renderdoc out of the debug_system.h and into a "real world" renderdoc_debug_system.cpp file that derives from debug_system.h and only loads a minimal amount of functionality at runtime to avoid tight integration requirements and still demonstrate what the tutorial is teaching. Fix the install_dependencies_windows.bat script by replacing the multi-line PowerShell command with a single line version which should hopefully fix the Windows issue. Address other issues as reported. --- attachments/simple_engine/CMakeLists.txt | 1 + .../simple_engine/camera_component.cpp | 2 +- attachments/simple_engine/debug_system.h | 51 ++------ .../install_dependencies_windows.bat | 11 +- .../simple_engine/renderdoc_debug_system.cpp | 121 ++++++++++++++++++ .../simple_engine/renderdoc_debug_system.h | 54 ++++++++ 6 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 attachments/simple_engine/renderdoc_debug_system.cpp create mode 100644 attachments/simple_engine/renderdoc_debug_system.h diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index a2a06819..6ab4e89c 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -119,6 +119,7 @@ set(SOURCES vulkan_device.cpp pipeline.cpp descriptor_manager.cpp + renderdoc_debug_system.cpp ) # Create executable diff --git a/attachments/simple_engine/camera_component.cpp b/attachments/simple_engine/camera_component.cpp index 6e73145e..45f1b9f5 100644 --- a/attachments/simple_engine/camera_component.cpp +++ b/attachments/simple_engine/camera_component.cpp @@ -41,7 +41,7 @@ void CameraComponent::UpdateViewMatrix() { glm::vec3 position = transformComponent->GetPosition(); viewMatrix = glm::lookAt(position, target, up); } else { - viewMatrix = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), target, up); + viewMatrix = glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), target, up); } viewMatrixDirty = false; } diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h index 5856491c..5681bea5 100644 --- a/attachments/simple_engine/debug_system.h +++ b/attachments/simple_engine/debug_system.h @@ -8,6 +8,7 @@ #include #include #include +#include /** * @brief Enum for different log levels. @@ -135,8 +136,8 @@ class DebugSystem { } // Call registered callbacks - for (const auto& callback : logCallbacks) { - callback(level, tag, message); + for (const auto& kv : logCallbacks) { + kv.second(level, tag, message); } // If fatal, trigger crash handler @@ -202,49 +203,15 @@ class DebugSystem { Log(LogLevel::Debug, "Performance", name + ": " + std::to_string(duration) + " us"); measurements.erase(it); } else { - Log(LogLevel::Warning, "Performance", "No measurement started with name: " + name); + Log(LogLevel::Error, "Performance", "No measurement started with name: " + name); } } - /** - * @brief Enable or disable RenderDoc integration. - * @param enable Whether to enable RenderDoc integration. - */ - void EnableRenderDoc(bool enable) { - std::lock_guard lock(mutex); - - renderDocEnabled = enable; - Log(LogLevel::Info, "DebugSystem", std::string("RenderDoc integration ") + (enable ? "enabled" : "disabled")); - - // In a real implementation, this would initialize RenderDoc API - } - - /** - * @brief Check if RenderDoc integration is enabled. - * @return True if RenderDoc integration is enabled, false otherwise. - */ - bool IsRenderDocEnabled() const { - return renderDocEnabled; - } - - /** - * @brief Trigger a RenderDoc frame capture. - */ - void CaptureRenderDocFrame() { - std::lock_guard lock(mutex); - - if (renderDocEnabled) { - Log(LogLevel::Info, "DebugSystem", "Capturing RenderDoc frame"); - - // In a real implementation, this would trigger a RenderDoc frame capture - } else { - Log(LogLevel::Warning, "DebugSystem", "RenderDoc integration is not enabled"); - } - } -private: - // Private constructor for singleton +protected: + // Protected constructor for inheritance DebugSystem() = default; + virtual ~DebugSystem() = default; // Delete copy constructor and assignment operator DebugSystem(const DebugSystem&) = delete; @@ -269,8 +236,6 @@ class DebugSystem { // Performance measurements std::unordered_map measurements; - // RenderDoc integration - bool renderDocEnabled = false; }; // Convenience macros for logging @@ -282,4 +247,4 @@ class DebugSystem { // Convenience macros for performance measurement #define MEASURE_START(name) DebugSystem::GetInstance().StartMeasurement(name) -#define MEASURE_END(name) DebugSystem::GetInstance().EndMeasurement(name) +#define MEASURE_END(name) DebugSystem::GetInstance().StopMeasurement(name) diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat index f1279a6b..84bff6d7 100644 --- a/attachments/simple_engine/install_dependencies_windows.bat +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -122,16 +122,7 @@ if defined SLANGC_EXE ( set "SLANG_ROOT=%LOCALAPPDATA%\slang" if not exist "%SLANG_ROOT%" mkdir "%SLANG_ROOT%" echo Downloading latest Slang release... - powershell -NoProfile -ExecutionPolicy Bypass -Command "\ -$ErrorActionPreference='Stop'; \ -$r=Invoke-RestMethod 'https://api.github.com/repos/shader-slang/slang/releases/latest'; \ -$asset=$r.assets | Where-Object { $_.name -match 'win64.*\\.zip$' } | Select-Object -First 1; \ -if(-not $asset){ throw 'No win64 asset found'; } \ -$out=Join-Path $env:TEMP $asset.name; \ -Invoke-WebRequest $asset.browser_download_url -OutFile $out; \ -Expand-Archive -Path $out -DestinationPath $env:LOCALAPPDATA\slang -Force; \ -Write-Host ('Downloaded Slang ' + $r.tag_name) \ -" + powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $r=Invoke-RestMethod 'https://api.github.com/repos/shader-slang/slang/releases/latest'; $asset=$r.assets | Where-Object { $_.name -match 'win64.*\.zip$' } | Select-Object -First 1; if(-not $asset){ throw 'No win64 asset found'; } $out=Join-Path $env:TEMP $asset.name; Invoke-WebRequest $asset.browser_download_url -OutFile $out; Expand-Archive -Path $out -DestinationPath $env:LOCALAPPDATA\slang -Force; Write-Host ('Downloaded Slang ' + $r.tag_name)" echo Locating slangc.exe... set "SLANGC_PATH=" for /f "delims=" %%F in ('dir /b /s "%LOCALAPPDATA%\slang\slangc.exe" 2^>nul') do ( diff --git a/attachments/simple_engine/renderdoc_debug_system.cpp b/attachments/simple_engine/renderdoc_debug_system.cpp new file mode 100644 index 00000000..9cd23ad9 --- /dev/null +++ b/attachments/simple_engine/renderdoc_debug_system.cpp @@ -0,0 +1,121 @@ +#include "renderdoc_debug_system.h" + +#include +#include + +#if defined(_WIN32) + #define WIN32_LEAN_AND_MEAN + #include +#elif defined(__APPLE__) || defined(__linux__) + #include +#endif + +// Value for eRENDERDOC_API_Version_1_4_1 from RenderDoc's header to avoid including it +#ifndef RENDERDOC_API_VERSION_1_4_1 +#define RENDERDOC_API_VERSION_1_4_1 10401 +#endif + +// Minimal local typedefs and struct to receive function pointers without including renderdoc_app.h +using pTriggerCaptureLocal = void (*)(); +using pStartFrameCaptureLocal = void (*)(void*, void*); +using pEndFrameCaptureLocal = unsigned int (*)(void*, void*); + +struct RENDERDOC_API_1_4_1_MIN { + pTriggerCaptureLocal TriggerCapture; + void* _pad0; // We don't rely on layout beyond the subset we read via memcpy + pStartFrameCaptureLocal StartFrameCapture; + pEndFrameCaptureLocal EndFrameCapture; +}; + +bool RenderDocDebugSystem::LoadRenderDocAPI() { + if (renderdocAvailable) return true; + + // Try to fetch RENDERDOC_GetAPI from a loaded module without forcing a dependency + pRENDERDOC_GetAPI getAPI = nullptr; + +#if defined(_WIN32) + HMODULE mod = GetModuleHandleA("renderdoc.dll"); + if (!mod) { + // If not already injected/loaded, do not force-load. We can attempt LoadLibraryA as a fallback + mod = LoadLibraryA("renderdoc.dll"); + if (!mod) { + LOG_INFO("RenderDoc", "RenderDoc not loaded into process"); + return false; + } + } + getAPI = reinterpret_cast(GetProcAddress(mod, "RENDERDOC_GetAPI")); +#elif defined(__APPLE__) || defined(__linux__) + void* mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); + if (!mod) { + // Try to load if not already loaded; if unavailable, just no-op + mod = dlopen("librenderdoc.so", RTLD_NOW); + if (!mod) { + LOG_INFO("RenderDoc", "RenderDoc not loaded into process"); + return false; + } + } + getAPI = reinterpret_cast(dlsym(mod, "RENDERDOC_GetAPI")); +#endif + + if (!getAPI) { + LOG_WARNING("RenderDoc", "RENDERDOC_GetAPI symbol not found"); + return false; + } + + // Request API 1.4.1 into a temporary buffer and then extract needed functions + RENDERDOC_API_1_4_1_MIN apiMin{}; + void* apiPtr = nullptr; + int result = getAPI(RENDERDOC_API_VERSION_1_4_1, &apiPtr); + if (result == 0 || apiPtr == nullptr) { + LOG_WARNING("RenderDoc", "Failed to acquire RenderDoc API 1.4.1"); + return false; + } + + // Copy only the subset we care about; layout is stable for these early members + std::memcpy(&apiMin, apiPtr, sizeof(apiMin)); + + fnTriggerCapture = apiMin.TriggerCapture; + fnStartFrameCapture = apiMin.StartFrameCapture; + fnEndFrameCapture = apiMin.EndFrameCapture; + + renderdocAvailable = (fnTriggerCapture || fnStartFrameCapture || fnEndFrameCapture); + + if (renderdocAvailable) { + LOG_INFO("RenderDoc", "RenderDoc API loaded"); + } else { + LOG_WARNING("RenderDoc", "RenderDoc API did not provide expected functions"); + } + + return renderdocAvailable; +} + +void RenderDocDebugSystem::TriggerCapture() { + if (!renderdocAvailable && !LoadRenderDocAPI()) return; + if (fnTriggerCapture) { + fnTriggerCapture(); + LOG_INFO("RenderDoc", "Triggered capture"); + } else { + LOG_WARNING("RenderDoc", "TriggerCapture not available"); + } +} + +void RenderDocDebugSystem::StartFrameCapture(void* device, void* window) { + if (!renderdocAvailable && !LoadRenderDocAPI()) return; + if (fnStartFrameCapture) { + fnStartFrameCapture(device, window); + LOG_DEBUG("RenderDoc", "StartFrameCapture called"); + } else { + LOG_WARNING("RenderDoc", "StartFrameCapture not available"); + } +} + +bool RenderDocDebugSystem::EndFrameCapture(void* device, void* window) { + if (!renderdocAvailable && !LoadRenderDocAPI()) return false; + if (fnEndFrameCapture) { + unsigned int ok = fnEndFrameCapture(device, window); + LOG_DEBUG("RenderDoc", ok ? "EndFrameCapture succeeded" : "EndFrameCapture failed"); + return ok != 0; + } + LOG_WARNING("RenderDoc", "EndFrameCapture not available"); + return false; +} diff --git a/attachments/simple_engine/renderdoc_debug_system.h b/attachments/simple_engine/renderdoc_debug_system.h new file mode 100644 index 00000000..e63b5bb0 --- /dev/null +++ b/attachments/simple_engine/renderdoc_debug_system.h @@ -0,0 +1,54 @@ +#pragma once + +#include "debug_system.h" + +// RenderDoc integration is optional and loaded at runtime. +// This header intentionally does NOT include to avoid a hard dependency. +// Instead, we declare a minimal interface and dynamically resolve the API if present. + +class RenderDocDebugSystem : public DebugSystem { +public: + static RenderDocDebugSystem& GetInstance() { + static RenderDocDebugSystem instance; + return instance; + } + + // Attempt to load the RenderDoc API from the current process. + // Safe to call multiple times. + bool LoadRenderDocAPI(); + + // Returns true if the RenderDoc API has been successfully loaded. + bool IsAvailable() const { return renderdocAvailable; } + + // Triggers an immediate capture (equivalent to pressing the capture hotkey in the UI). + void TriggerCapture(); + + // Starts a frame capture for the given device/window (can be nullptr to auto-detect on many backends). + void StartFrameCapture(void* device = nullptr, void* window = nullptr); + + // Ends a frame capture previously started. Returns true on success. + bool EndFrameCapture(void* device = nullptr, void* window = nullptr); + +private: + RenderDocDebugSystem() = default; + ~RenderDocDebugSystem() override = default; + + RenderDocDebugSystem(const RenderDocDebugSystem&) = delete; + RenderDocDebugSystem& operator=(const RenderDocDebugSystem&) = delete; + + // Internal function pointers matching the subset of RenderDoc API we use. + // We avoid including the official header by declaring minimal signatures. + using pRENDERDOC_GetAPI = int (*)(int, void**); + + // Subset of API function pointers + typedef void (*pRENDERDOC_TriggerCapture)(); + typedef void (*pRENDERDOC_StartFrameCapture)(void* device, void* window); + typedef unsigned int (*pRENDERDOC_EndFrameCapture)(void* device, void* window); // returns bool in C API + + // Storage for resolved API + pRENDERDOC_TriggerCapture fnTriggerCapture = nullptr; + pRENDERDOC_StartFrameCapture fnStartFrameCapture = nullptr; + pRENDERDOC_EndFrameCapture fnEndFrameCapture = nullptr; + + bool renderdocAvailable = false; +}; From a5edf28b5ad8fcf6a8803755cf0048b9f73f6b3e Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 11 Aug 2025 01:54:33 -0700 Subject: [PATCH 049/102] the lighting in the PBR scene *looks* better. There's still a bit that doesn't quite feel right. Need to investigate. But position was getting set close to the origin so something screwy is still going on. --- .../simple_engine/descriptor_manager.cpp | 16 +++-- attachments/simple_engine/model_loader.cpp | 30 +++++---- attachments/simple_engine/physics_system.cpp | 6 +- attachments/simple_engine/renderer.h | 2 +- .../simple_engine/renderer_resources.cpp | 13 +--- attachments/simple_engine/shaders/pbr.slang | 62 ++++++++++++------- 6 files changed, 67 insertions(+), 62 deletions(-) diff --git a/attachments/simple_engine/descriptor_manager.cpp b/attachments/simple_engine/descriptor_manager.cpp index bb46b794..e75f7830 100644 --- a/attachments/simple_engine/descriptor_manager.cpp +++ b/attachments/simple_engine/descriptor_manager.cpp @@ -52,14 +52,12 @@ bool DescriptorManager::createUniformBuffers(Entity* entity, uint32_t maxFramesI vk::DeviceSize bufferSize = sizeof(UniformBufferObject); // Create entity resources if they don't exist - if (entityResources.find(entity) == entityResources.end()) { - entityResources[entity] = EntityResources(); - } + auto entityResourcesIt = entityResources.try_emplace( entity ).first; // Clear existing uniform buffers - entityResources[entity].uniformBuffers.clear(); - entityResources[entity].uniformBuffersMemory.clear(); - entityResources[entity].uniformBuffersMapped.clear(); + entityResourcesIt->second.uniformBuffers.clear(); + entityResourcesIt->second.uniformBuffersMemory.clear(); + entityResourcesIt->second.uniformBuffersMapped.clear(); // Create uniform buffers for (size_t i = 0; i < maxFramesInFlight; i++) { @@ -74,9 +72,9 @@ bool DescriptorManager::createUniformBuffers(Entity* entity, uint32_t maxFramesI void* data = bufferMemory.mapMemory(0, bufferSize); // Store resources - entityResources[entity].uniformBuffers.push_back(std::move(buffer)); - entityResources[entity].uniformBuffersMemory.push_back(std::move(bufferMemory)); - entityResources[entity].uniformBuffersMapped.push_back(data); + entityResourcesIt->second.uniformBuffers.push_back(std::move(buffer)); + entityResourcesIt->second.uniformBuffersMemory.push_back(std::move(bufferMemory)); + entityResourcesIt->second.uniformBuffersMapped.push_back(data); } return true; diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 52a6475e..ed68771e 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -41,7 +41,7 @@ static bool LoadKTX2FileToRGBA(const std::string& filePath, std::vector // Emissive scaling factor to convert from Blender units to engine units #define EMISSIVE_SCALE_FACTOR (1.0f / 638.0f) -#define LIGHT_SCALE_FACTOR (1.0f / 2.0f) // The sun is way too bright +#define LIGHT_SCALE_FACTOR (1.0f / 638.0f) ModelLoader::~ModelLoader() { // Destructor implementation @@ -184,6 +184,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cout << "Blender generator detected, applying blender factor" << std::endl; light_scale = EMISSIVE_SCALE_FACTOR; } + light_scale = EMISSIVE_SCALE_FACTOR; // Track loaded textures to prevent loading the same texture multiple times std::set loadedTextures; @@ -1061,8 +1062,19 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { } else { std::cerr << " Failed to load embedded normal texture: " << textureId << std::endl; } + } else if (!image.uri.empty()) { + // Fallback: load KTX2 from a file and upload to memory + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + materialMesh.normalTexturePath = textureId; + std::cout << " Loaded normal KTX2 file: " << filePath << std::endl; + } else { + std::cerr << " Failed to load normal KTX2 file: " << filePath << std::endl; + } } else { - std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; + std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; } } } @@ -1265,7 +1277,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { std::cout << "Extracting lights from GLTF model..." << std::endl; // Extract punctual lights (KHR_lights_punctual extension) - if (!ExtractPunctualLights(gltfModel, filename)) { + if (ExtractPunctualLights(gltfModel, filename)) { std::cerr << "Warning: Failed to extract punctual lights from " << filename << std::endl; } @@ -1294,7 +1306,7 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m // Check if this material has emissive properties (no threshold filtering) float emissiveIntensity = glm::length(material->emissive) * material->emissiveStrength; - if (emissiveIntensity >= 0.0f) { // Accept all emissive materials, including zero intensities + if (emissiveIntensity >= 0.1f) { // Calculate the center position of the emissive surface glm::vec3 center(0.0f); if (!materialMesh.vertices.empty()) { @@ -1315,18 +1327,13 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m avgNormal = glm::vec3(0.0f, -1.0f, 0.0f); // Default downward direction } - // CRITICAL FIX: Offset the light position away from the surface - // This allows the emissive light to properly illuminate the surface from outside - float offsetDistance = 0.5f; // Offset distance from surface - glm::vec3 lightPosition = center + avgNormal * offsetDistance; - // Create an emissive light source ExtractedLight emissiveLight; emissiveLight.type = ExtractedLight::Type::Emissive; - emissiveLight.position = lightPosition; // Use the offset position + emissiveLight.position = center; emissiveLight.color = material->emissive; emissiveLight.intensity = material->emissiveStrength; - emissiveLight.range = 10.0f; // Default range for emissive lights + emissiveLight.range = 1.0f; // Default range for emissive lights emissiveLight.sourceMaterial = material->GetName(); emissiveLight.direction = avgNormal; @@ -1485,4 +1492,3 @@ bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const std::cout << " Extracted " << lights.size() << " total lights from model" << std::endl; return lights.empty(); } - diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index 04020a54..d84c5e09 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -1158,15 +1158,13 @@ void PhysicsSystem::ReadbackGPUPhysicsData() const { if (vulkanResources.persistentCounterMemory) { static uint32_t lastPairCount = UINT32_MAX; static uint32_t lastCollisionCount = UINT32_MAX; - static int debugFrames = 0; const uint32_t* counters = static_cast(vulkanResources.persistentCounterMemory); uint32_t pairCount = counters[0]; uint32_t collisionCount = counters[1]; - if (debugFrames < 120 && (pairCount != lastPairCount || collisionCount != lastCollisionCount)) { - std::cout << "Physics GPU counters - pairs: " << pairCount << ", collisions: " << collisionCount << std::endl; + if (pairCount != lastPairCount || collisionCount != lastCollisionCount) { + // std::cout << "Physics GPU counters - pairs: " << pairCount << ", collisions: " << collisionCount << std::endl; lastPairCount = pairCount; lastCollisionCount = collisionCount; - debugFrames++; } } diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 5f5676fa..125aef7a 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -45,7 +45,7 @@ struct SwapChainSupportDetails { * @brief Structure for individual light data in the storage buffer. */ struct LightData { - alignas(16) glm::vec4 position; // Light position (w component unused) + alignas(16) glm::vec4 position; // Light position (w component used for direction vs position) alignas(16) glm::vec4 color; // Light color and intensity alignas(16) glm::mat4 lightSpaceMatrix; // Light space matrix for shadow mapping alignas(4) int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 2aa8c742..98c068e6 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1218,13 +1218,6 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa descriptor_path_resolved: ; } - // Debug: report resolved baseColor texture binding for this entity - if (meshComponent) { - const std::string& bc = pbrTexturePaths[0]; - bool present = textureResources.find(bc) != textureResources.end(); - std::cout << "PBR baseColor for entity '" << entity->GetName() << "': '" << bc - << "' (" << (present ? "loaded" : "not in cache") << ")" << std::endl; - } // Create descriptor writes for all 5 texture bindings for (int binding = 1; binding <= 5; binding++) { descriptorWrites[binding] = vk::WriteDescriptorSet{ @@ -1934,11 +1927,7 @@ bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector Date: Tue, 12 Aug 2025 02:48:29 -0700 Subject: [PATCH 050/102] Remove shadows for now. We'll concentrate on them in the samples version. Add ability to set time of day so can experience the night with nice lights. Still have a bug in the emissive light textures as it looks like the bulbs aren't quite working right. --- attachments/simple_engine/CMakeLists.txt | 1 - attachments/simple_engine/imgui_system.cpp | 26 +-- attachments/simple_engine/model_loader.cpp | 160 +++++++++++++----- attachments/simple_engine/renderer.h | 63 +++---- attachments/simple_engine/renderer_core.cpp | 23 --- .../simple_engine/renderer_pipelines.cpp | 10 +- .../simple_engine/renderer_rendering.cpp | 95 ++++++++++- .../simple_engine/renderer_resources.cpp | 48 ++---- .../simple_engine/renderer_shadows.cpp | 140 --------------- attachments/simple_engine/shaders/pbr.slang | 106 +++--------- 10 files changed, 273 insertions(+), 399 deletions(-) delete mode 100644 attachments/simple_engine/renderer_shadows.cpp diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 6ab4e89c..488b61cc 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -102,7 +102,6 @@ set(SOURCES renderer_compute.cpp renderer_utils.cpp renderer_resources.cpp - renderer_shadows.cpp memory_pool.cpp resource_manager.cpp entity.cpp diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index b455e9b0..206b6654 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -169,22 +169,28 @@ void ImGuiSystem::NewFrame() { std::cout << "Exposure reset to: " << exposure << std::endl; } - // Shadow toggle - static bool shadowsEnabled = true; // Default shadows on - if (ImGui::Checkbox("Enable Shadows", &shadowsEnabled)) { - // Update shadows in renderer - if (renderer) { - renderer->SetShadowsEnabled(shadowsEnabled); - } - std::cout << "Shadows " << (shadowsEnabled ? "enabled" : "disabled") << std::endl; - } - ImGui::Text("Tip: Adjust gamma if scene looks too dark/bright"); ImGui::Text("Tip: Adjust exposure if scene looks washed out"); } else { ImGui::Text("Note: Quality controls affect BRDF rendering only"); } + ImGui::Separator(); + + // Sun position control (punctual light in GLTF) + ImGui::Text("Sun Position in Sky:"); + if (renderer) { + float sun = renderer->GetSunPosition(); + if (ImGui::SliderFloat("Sun Position", &sun, 0.0f, 1.0f, "%.2f")) { + renderer->SetSunPosition(sun); + } + ImGui::SameLine(); + if (ImGui::Button("Noon")) { sun = 0.5f; renderer->SetSunPosition(sun); } + ImGui::SameLine(); + if (ImGui::Button("Night")) { sun = 0.0f; renderer->SetSunPosition(sun); } + ImGui::Text("Tip: 0/1 = Night, 0.5 = Noon. Warmer tint near horizon simulates evening."); + } + ImGui::Separator(); ImGui::Text("3D Audio Position Control"); diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index ed68771e..7ce6fdf4 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -215,7 +215,7 @@ bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { gltfMaterial.emissiveFactor[0], gltfMaterial.emissiveFactor[1], gltfMaterial.emissiveFactor[2] - ) * light_scale; + ); } // Parse KHR_materials_emissive_strength extension @@ -1327,21 +1327,46 @@ std::vector ModelLoader::GetExtractedLights(const std::string& m avgNormal = glm::vec3(0.0f, -1.0f, 0.0f); // Default downward direction } - // Create an emissive light source - ExtractedLight emissiveLight; - emissiveLight.type = ExtractedLight::Type::Emissive; - emissiveLight.position = center; - emissiveLight.color = material->emissive; - emissiveLight.intensity = material->emissiveStrength; - emissiveLight.range = 1.0f; // Default range for emissive lights - emissiveLight.sourceMaterial = material->GetName(); - emissiveLight.direction = avgNormal; - - lights.push_back(emissiveLight); - - std::cout << "Created emissive light from material '" << material->GetName() - << "' at position (" << center.x << ", " << center.y << ", " << center.z - << ") with intensity " << emissiveIntensity << std::endl; + // Create emissive light(s) transformed by each instance's model matrix + if (!materialMesh.instances.empty()) { + for (const auto& inst : materialMesh.instances) { + glm::mat4 M = inst.getModelMatrix(); + glm::vec3 worldCenter = glm::vec3(M * glm::vec4(center, 1.0f)); + glm::mat3 normalMat = glm::transpose(glm::inverse(glm::mat3(M))); + glm::vec3 worldNormal = glm::normalize(normalMat * avgNormal); + + ExtractedLight emissiveLight; + emissiveLight.type = ExtractedLight::Type::Emissive; + emissiveLight.position = worldCenter; + emissiveLight.color = material->emissive; + emissiveLight.intensity = material->emissiveStrength; + emissiveLight.range = 1.0f; // Default range for emissive lights + emissiveLight.sourceMaterial = material->GetName(); + emissiveLight.direction = worldNormal; + + lights.push_back(emissiveLight); + + std::cout << "Created emissive light from material '" << material->GetName() + << "' at world position (" << worldCenter.x << ", " << worldCenter.y << ", " << worldCenter.z + << ") with intensity " << emissiveIntensity << std::endl; + } + } else { + // No explicit instances; use identity transform + ExtractedLight emissiveLight; + emissiveLight.type = ExtractedLight::Type::Emissive; + emissiveLight.position = center; + emissiveLight.color = material->emissive; + emissiveLight.intensity = material->emissiveStrength; + emissiveLight.range = 1.0f; // Default range for emissive lights + emissiveLight.sourceMaterial = material->GetName(); + emissiveLight.direction = avgNormal; + + lights.push_back(emissiveLight); + + std::cout << "Created emissive light from material '" << material->GetName() + << "' at position (" << center.x << ", " << center.y << ", " << center.z + << ") with intensity " << emissiveIntensity << std::endl; + } } } } @@ -1446,35 +1471,90 @@ bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const std::cout << " No KHR_lights_punctual extension found" << std::endl; } - // Now find light nodes in the scene to get positions and directions - for (const auto& node : gltfModel.nodes) { + // Compute world transforms for all nodes in the default scene + std::vector nodeWorldTransforms(gltfModel.nodes.size(), glm::mat4(1.0f)); + + auto calcLocal = [](const tinygltf::Node& n) -> glm::mat4 { + // If matrix is provided, use it + if (n.matrix.size() == 16) { + glm::mat4 m(1.0f); + for (int r = 0; r < 4; ++r) { + for (int c = 0; c < 4; ++c) { + m[c][r] = static_cast(n.matrix[r * 4 + c]); + } + } + return m; + } + // Otherwise compose TRS + glm::mat4 T(1.0f), R(1.0f), S(1.0f); + if (n.translation.size() == 3) { + T = glm::translate(glm::mat4(1.0f), glm::vec3( + static_cast(n.translation[0]), + static_cast(n.translation[1]), + static_cast(n.translation[2]))); + } + if (n.rotation.size() == 4) { + glm::quat q( + static_cast(n.rotation[3]), + static_cast(n.rotation[0]), + static_cast(n.rotation[1]), + static_cast(n.rotation[2])); + R = glm::mat4_cast(q); + } + if (n.scale.size() == 3) { + S = glm::scale(glm::mat4(1.0f), glm::vec3( + static_cast(n.scale[0]), + static_cast(n.scale[1]), + static_cast(n.scale[2]))); + } + return T * R * S; + }; + + std::function traverseNode = [&](int nodeIndex, const glm::mat4& parent) { + if (nodeIndex < 0 || nodeIndex >= static_cast(gltfModel.nodes.size())) return; + const tinygltf::Node& n = gltfModel.nodes[nodeIndex]; + glm::mat4 local = calcLocal(n); + glm::mat4 world = parent * local; + nodeWorldTransforms[nodeIndex] = world; + for (int child : n.children) { + traverseNode(child, world); + } + }; + + if (!gltfModel.scenes.empty()) { + int sceneIndex = gltfModel.defaultScene >= 0 ? gltfModel.defaultScene : 0; + if (sceneIndex < static_cast(gltfModel.scenes.size())) { + const tinygltf::Scene& scene = gltfModel.scenes[sceneIndex]; + for (int root : scene.nodes) { + traverseNode(root, glm::mat4(1.0f)); + } + } + } else { + // Fallback: traverse all nodes as roots + for (int i = 0; i < static_cast(gltfModel.nodes.size()); ++i) { + traverseNode(i, glm::mat4(1.0f)); + } + } + + // Now assign positions and directions using world transforms + for (size_t nodeIndex = 0; nodeIndex < gltfModel.nodes.size(); ++nodeIndex) { + const auto& node = gltfModel.nodes[nodeIndex]; if (node.extensions.contains("KHR_lights_punctual")) { const tinygltf::Value& nodeExtension = node.extensions.at("KHR_lights_punctual"); if (nodeExtension.Has("light") && nodeExtension.Get("light").IsInt()) { int lightIndex = nodeExtension.Get("light").Get(); if (lightIndex >= 0 && lightIndex < static_cast(lights.size())) { - // Extract position from node transform - if (node.translation.size() >= 3) { - lights[lightIndex].position = glm::vec3( - static_cast(node.translation[0]), - static_cast(node.translation[1]), - static_cast(node.translation[2]) - ); - } - - // Extract direction from node rotation (for directional and spotlights) - if (node.rotation.size() >= 4 && - (lights[lightIndex].type == ExtractedLight::Type::Directional || - lights[lightIndex].type == ExtractedLight::Type::Spot)) { - // Convert quaternion to a direction vector - glm::quat rotation( - static_cast(node.rotation[3]), // w - static_cast(node.rotation[0]), // x - static_cast(node.rotation[1]), // y - static_cast(node.rotation[2]) // z - ); - // Default forward direction in glTF is -Z - lights[lightIndex].direction = rotation * glm::vec3(0.0f, 0.0f, -1.0f); + const glm::mat4& W = nodeWorldTransforms[nodeIndex]; + // Position from world transform origin + glm::vec3 pos = glm::vec3(W * glm::vec4(0, 0, 0, 1)); + lights[lightIndex].position = pos; + + // Direction for directional/spot: transform -Z + if (lights[lightIndex].type == ExtractedLight::Type::Directional || + lights[lightIndex].type == ExtractedLight::Type::Spot) { + glm::mat3 rot = glm::mat3(W); + glm::vec3 dir = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f)); + lights[lightIndex].direction = dir; } std::cout << " Light " << lightIndex << " positioned at (" diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 125aef7a..7d7e2b4f 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "platform.h" #include "entity.h" @@ -67,13 +68,13 @@ struct UniformBufferObject { alignas(4) float prefilteredCubeMipLevels; alignas(4) float scaleIBLAmbient; alignas(4) int lightCount; // Number of active lights (dynamic) - alignas(4) int shadowMapCount; // Number of active shadow maps (dynamic) - alignas(4) float shadowBias; // Shadow bias to prevent shadow acne + alignas(4) int padding0; // Padding for alignment (shadows removed) alignas(4) float padding1; // Padding for alignment + alignas(4) float padding2; // Padding for alignment // Additional padding to ensure the structure size is aligned to 64 bytes (device nonCoherentAtomSize) - // Current size: 3*64 + 16 + 8*4 = 240 bytes, pad to 256 bytes (multiple of 64) - alignas(4) float padding2[4]; // Add 16 more bytes to reach 256 bytes total + // Adjusted padding to maintain 256 bytes total size + alignas(4) float padding3[2]; // Add remaining bytes to reach 256 bytes total }; @@ -162,6 +163,16 @@ class Renderer { */ bool IsInitialized() const { return initialized; } + /** + * @brief Set sun position slider value in [0,1]. 0 and 1 = night, 0.5 = noon. + */ + void SetSunPosition(float s) { sunPosition = std::clamp(s, 0.0f, 1.0f); } + + /** + * @brief Get sun position slider value. + */ + float GetSunPosition() const { return sunPosition; } + /** * @brief Get the Vulkan device. @@ -347,21 +358,6 @@ class Renderer { exposure = _exposure; } - /** - * @brief Enable or disable shadow rendering. - * @param enabled True to enable shadows, false to disable. - * @note Shadows should be pre-computed and cached during model loading for optimal performance. - * Toggling this flag should only affect shader uniform values, not trigger recomputation. - */ - void SetShadowsEnabled(bool enabled) { - // Only update if the value actually changed to avoid unnecessary uniform buffer updates - if (this->shadowsEnabled != enabled) { - this->shadowsEnabled = enabled; - // TODO: Update uniform buffer with shadow enable/disable flag - // Shadow maps should remain cached and not be recomputed - } - } - /** * @brief Create or resize light storage buffers to accommodate the given number of lights. * @param lightCount The number of lights to accommodate. @@ -417,7 +413,9 @@ class Renderer { // PBR rendering parameters float gamma = 2.2f; // Gamma correction value float exposure = 3.0f; // HDR exposure value (higher for emissive lighting) - bool shadowsEnabled = true; // Shadow rendering enabled by default + + // Sun control (UI-driven) + float sunPosition = 0.5f; // 0..1, extremes are night, 0.5 is noon // Vulkan RAII context vk::raii::Context context; @@ -519,25 +517,8 @@ class Renderer { // Default texture resources (used when no texture is provided) TextureResources defaultTextureResources; - // Shadow mapping resources - struct ShadowMapResources { - vk::raii::Image shadowMapImage = nullptr; - std::unique_ptr shadowMapImageAllocation = nullptr; - vk::raii::ImageView shadowMapImageView = nullptr; - vk::raii::Sampler shadowMapSampler = nullptr; - vk::raii::Framebuffer shadowMapFramebuffer = nullptr; - vk::raii::RenderPass shadowMapRenderPass = nullptr; - uint32_t shadowMapSize = 2048; // Default shadow map resolution - }; - std::vector shadowMaps; // One shadow map per light - - // Shadow mapping constants - static constexpr uint32_t MAX_SHADOW_MAPS = 16; // Descriptors/array size remains 16 - static constexpr uint32_t DEFAULT_SHADOW_MAP_SIZE = 2048; - // Performance clamps (to reduce per-frame cost) - static constexpr uint32_t MAX_ACTIVE_LIGHTS = 32; // Limit the number of lights processed per frame - static constexpr uint32_t MAX_SHADOW_MAPS_USED = 4; // Limit the number of shadows sampled per frame + static constexpr uint32_t MAX_ACTIVE_LIGHTS = 1024; // Limit the number of lights processed per frame // Static lights loaded during model initialization std::vector staticLights; @@ -627,12 +608,6 @@ class Renderer { bool createCommandPool(); // Shadow mapping methods - bool createShadowMaps(); - bool createShadowMapRenderPass(); - bool createShadowMapFramebuffers(); - bool createShadowMapDescriptorSetLayout(); - void renderShadowMaps(const std::vector& entities, const std::vector& lights); - void updateShadowMapUniforms(uint32_t lightIndex, const ExtractedLight& light); bool createComputeCommandPool(); bool createDepthResources(); bool createTextureImage(const std::string& texturePath, TextureResources& resources); diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 76f1805d..ecd14d8b 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -162,29 +162,6 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Create shadow maps for shadow mapping - if (!createShadowMaps()) { - std::cerr << "Failed to create shadow maps" << std::endl; - return false; - } - - // Create a shadow map render pass - if (!createShadowMapRenderPass()) { - std::cerr << "Failed to create shadow map render pass" << std::endl; - return false; - } - - // Create shadow map framebuffers - if (!createShadowMapFramebuffers()) { - std::cerr << "Failed to create shadow map framebuffers" << std::endl; - return false; - } - - // Create a shadow map descriptor set layout - if (!createShadowMapDescriptorSetLayout()) { - std::cerr << "Failed to create shadow map descriptor set layout" << std::endl; - return false; - } // Create command buffers if (!createCommandBuffers()) { diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index c40e2be6..ef3375a0 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -95,17 +95,9 @@ bool Renderer::createPBRDescriptorSetLayout() { .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr }, - // Binding 6: Shadow map array + // Binding 6: Light storage buffer (shadows removed) vk::DescriptorSetLayoutBinding{ .binding = 6, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .descriptorCount = MAX_SHADOW_MAPS, // Array of shadow maps (dynamic count) - .stageFlags = vk::ShaderStageFlagBits::eFragment, - .pImmutableSamplers = nullptr - }, - // Binding 7: Light storage buffer - vk::DescriptorSetLayoutBinding{ - .binding = 7, .descriptorType = vk::DescriptorType::eStorageBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 034c2342..3d4bd2ee 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include // This file contains rendering-related methods from the Renderer class @@ -395,23 +397,98 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity lightsSubset.push_back(extractedLights[i]); } + // Apply UI-driven sun control to the first directional light with a Paris-based solar path + { + // Find first directional light to treat as the Sun + size_t sunIdx = SIZE_MAX; + for (size_t i = 0; i < lightsSubset.size(); ++i) { + if (lightsSubset[i].type == ExtractedLight::Type::Directional) { sunIdx = i; break; } + } + if (sunIdx != SIZE_MAX) { + auto &sun = lightsSubset[sunIdx]; + float s = std::clamp(sunPosition, 0.0f, 1.0f); + + // Paris latitude (degrees) + const float latDeg = 48.8566f; + const float lat = latDeg * 0.01745329251994329577f; // radians + + // Get current day-of-year (0..365) for declination + int yday = 172; // Default to around June solstice if time is unavailable + std::time_t now = std::time(nullptr); + if (now != (std::time_t)(-1)) { + std::tm localTm{}; + #ifdef _WIN32 + localtime_s(&localTm, &now); + #else + std::tm* ptm = std::localtime(&now); + if (ptm) localTm = *ptm; + #endif + if (localTm.tm_yday >= 0) yday = localTm.tm_yday; // 0-based + } + + // Solar declination (degrees) using Cooper's approximation + // δ = 23.45° * sin(360° * (284 + n) / 365) + float declDeg = 23.45f * std::sin((6.283185307179586f) * (284.0f + (float)(yday + 1)) / 365.0f); + float decl = declDeg * 0.01745329251994329577f; // radians + + // Map slider to local solar time (0..24h), hour angle H in radians (0 at noon) + float hours = s * 24.0f; + float Hdeg = (hours - 12.0f) * 15.0f; // degrees per hour + float H = Hdeg * 0.01745329251994329577f; // radians + + // Solar altitude (elevation) from spherical astronomy + // sin(alt) = sin φ sin δ + cos φ cos δ cos H + float sinAlt = std::sin(lat) * std::sin(decl) + std::cos(lat) * std::cos(decl) * std::cos(H); + sinAlt = std::clamp(sinAlt, -1.0f, 1.0f); + float alt = std::asin(sinAlt); // radians + + // Build horizontal azimuth basis from original sun direction (treat original as local solar noon azimuth) + glm::vec3 origDir = sun.direction; + glm::vec2 baseHoriz2 = glm::normalize(glm::vec2(origDir.x, origDir.z)); + if (!std::isfinite(baseHoriz2.x)) { baseHoriz2 = glm::vec2(0.0f, -1.0f); } + + // Rotate base horizontal around Y by hour angle H (east-west movement). Positive H -> afternoon (west) + float cosH = std::cos(H); + float sinH = std::sin(H); + glm::vec2 horizRot2 = glm::normalize(glm::vec2( + baseHoriz2.x * cosH - baseHoriz2.y * sinH, + baseHoriz2.x * sinH + baseHoriz2.y * cosH)); + + // Compose final direction from altitude and rotated horizontal + float cosAlt = std::cos(alt); + float sinAltClamped = std::sin(alt); + glm::vec3 newDir = glm::normalize(glm::vec3(horizRot2.x * cosAlt, -sinAltClamped, horizRot2.y * cosAlt)); + sun.direction = newDir; + + // Intensity scales with daylight (altitude); zero when below horizon + float dayFactor = std::max(0.0f, sinAltClamped); // 0..1 roughly + + // Warm tint increases near horizon when sun is above horizon + float horizonFactor = 0.0f; + if (sinAltClamped > 0.0f) { + // More warmth for low altitude, fade to zero near high noon + float normAlt = std::clamp(alt / (1.57079632679f), 0.0f, 1.0f); // 0 at horizon, 1 at zenith + horizonFactor = 1.0f - normAlt; // 1 near horizon, 0 at zenith + } + glm::vec3 warm(1.0f, 0.75f, 0.55f); + float tintAmount = 0.7f * horizonFactor; + sun.color = glm::mix(sun.color, warm, tintAmount); + + // Apply intensity scaling (preserve original magnitude shape) + sun.intensity *= dayFactor; + } + } + // Update the light storage buffer with the subset of light data updateLightStorageBuffer(currentImage, lightsSubset); ubo.lightCount = static_cast(numLights); - // Set shadow map count based on shadowsEnabled flag with a tighter cap - if (shadowsEnabled) { - ubo.shadowMapCount = static_cast(std::min(numLights, size_t(MAX_SHADOW_MAPS_USED))); - } else { - ubo.shadowMapCount = 0; // Disable shadows when shadowsEnabled is false - } + // Shadows removed: no shadow maps } else { ubo.lightCount = 0; - ubo.shadowMapCount = 0; } - // Set shadow mapping parameters - ubo.shadowBias = 0.005f; // Adjust to prevent shadow acne + // Shadows removed: no shadow bias // Set camera position for PBR calculations ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 98c068e6..36c34e34 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1064,10 +1064,9 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa }; if (usePBR) { - // PBR pipeline: Create 8 descriptor writes (UBO + 5 textures + shadow map array + light storage buffer) - std::array descriptorWrites; + // PBR pipeline: Create 7 descriptor writes (UBO + 5 textures + light storage buffer) + std::array descriptorWrites; std::array imageInfos; - std::array shadowMapInfos; // Uniform buffer descriptor writes (binding 0) descriptorWrites[0] = vk::WriteDescriptorSet{ @@ -1230,36 +1229,9 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa }; } - // Create shadow map image infos for binding 6 - for (int j = 0; j < 16; j++) { - if (j < static_cast(shadowMaps.size()) && *shadowMaps[j].shadowMapImageView) { - // Use actual shadow map - shadowMapInfos[j] = vk::DescriptorImageInfo{ - .sampler = *shadowMaps[j].shadowMapSampler, - .imageView = *shadowMaps[j].shadowMapImageView, - .imageLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal - }; - } else { - // Use default texture as placeholder for unused shadow map slots - shadowMapInfos[j] = vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - } - } - - // Create descriptor write for shadow map array (binding 6) - descriptorWrites[6] = vk::WriteDescriptorSet{ - .dstSet = targetDescriptorSets[i], - .dstBinding = 6, - .dstArrayElement = 0, - .descriptorCount = 16, // Array of 16 shadow maps - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = shadowMapInfos.data() - }; + // No shadow maps: binding 6 is now the light storage buffer - // Create descriptor write for light storage buffer (binding 7) + // Create descriptor write for light storage buffer (binding 6) // Check if light storage buffers are initialized if (i < lightStorageBuffers.size() && *lightStorageBuffers[i].buffer) { vk::DescriptorBufferInfo lightBufferInfo{ @@ -1268,9 +1240,9 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .range = VK_WHOLE_SIZE }; - descriptorWrites[7] = vk::WriteDescriptorSet{ + descriptorWrites[6] = vk::WriteDescriptorSet{ .dstSet = targetDescriptorSets[i], - .dstBinding = 7, + .dstBinding = 6, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, @@ -1291,9 +1263,9 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa .range = VK_WHOLE_SIZE }; - descriptorWrites[7] = vk::WriteDescriptorSet{ + descriptorWrites[6] = vk::WriteDescriptorSet{ .dstSet = targetDescriptorSets[i], - .dstBinding = 7, + .dstBinding = 6, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, @@ -1301,7 +1273,7 @@ bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePa }; } - // Update descriptor sets with all 8 descriptors + // Update descriptor sets with all 7 descriptors device.updateDescriptorSets(descriptorWrites, {}); } else { // Basic pipeline: Create 2 descriptor writes (UBO + 1 texture) @@ -1875,7 +1847,7 @@ void Renderer::updateAllDescriptorSetsWithNewLightBuffers() { vk::WriteDescriptorSet descriptorWrite{ .dstSet = *resources.pbrDescriptorSets[i], - .dstBinding = 7, + .dstBinding = 6, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, diff --git a/attachments/simple_engine/renderer_shadows.cpp b/attachments/simple_engine/renderer_shadows.cpp deleted file mode 100644 index f1272257..00000000 --- a/attachments/simple_engine/renderer_shadows.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "renderer.h" -#include "model_loader.h" -#include -#include - -// This file contains shadow mapping implementation for the Renderer class - -bool Renderer::createShadowMaps() { - try { - std::cout << "Creating shadow maps..." << std::endl; - - // Initialize shadow maps vector - limit to 16 for performance/memory - const uint32_t ACTUAL_SHADOW_MAPS = 16; - shadowMaps.resize(ACTUAL_SHADOW_MAPS); - - for (uint32_t i = 0; i < ACTUAL_SHADOW_MAPS; ++i) { - auto& shadowMap = shadowMaps[i]; - - // Create shadow map image using memory pool - auto [shadowImg, shadowImgAllocation] = createImagePooled( - DEFAULT_SHADOW_MAP_SIZE, - DEFAULT_SHADOW_MAP_SIZE, - vk::Format::eD32Sfloat, // 32-bit depth format for high precision - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - shadowMap.shadowMapImage = std::move(shadowImg); - shadowMap.shadowMapImageAllocation = std::move(shadowImgAllocation); - shadowMap.shadowMapSize = DEFAULT_SHADOW_MAP_SIZE; - - // Create shadow map image view - shadowMap.shadowMapImageView = createImageView( - shadowMap.shadowMapImage, - vk::Format::eD32Sfloat, - vk::ImageAspectFlagBits::eDepth - ); - - // Create shadow map sampler - vk::SamplerCreateInfo samplerInfo{ - .magFilter = vk::Filter::eLinear, - .minFilter = vk::Filter::eLinear, - .mipmapMode = vk::SamplerMipmapMode::eLinear, - .addressModeU = vk::SamplerAddressMode::eClampToBorder, - .addressModeV = vk::SamplerAddressMode::eClampToBorder, - .addressModeW = vk::SamplerAddressMode::eClampToBorder, - .mipLodBias = 0.0f, - .anisotropyEnable = VK_FALSE, - .maxAnisotropy = 1.0f, - .compareEnable = VK_TRUE, // Enable depth comparison for shadow mapping - .compareOp = vk::CompareOp::eLessOrEqual, - .minLod = 0.0f, - .maxLod = 1.0f, - .borderColor = vk::BorderColor::eFloatOpaqueWhite, // White border = no shadow - .unnormalizedCoordinates = VK_FALSE - }; - - shadowMap.shadowMapSampler = vk::raii::Sampler(device, samplerInfo); - - // Transition shadow map to read-only layout for shader sampling - // Shadow maps will be transitioned to attachment layout when rendering, then back to read-only - transitionImageLayout( - *shadowMap.shadowMapImage, - vk::Format::eD32Sfloat, - vk::ImageLayout::eUndefined, - vk::ImageLayout::eDepthStencilReadOnlyOptimal - ); - - std::cout << " Created shadow map " << i << " (" << DEFAULT_SHADOW_MAP_SIZE << "x" << DEFAULT_SHADOW_MAP_SIZE << ")" << std::endl; - } - - std::cout << "Successfully created " << ACTUAL_SHADOW_MAPS << " shadow maps" << std::endl; - return true; - - } catch (const std::exception& e) { - std::cerr << "Failed to create shadow maps: " << e.what() << std::endl; - return false; - } -} - -bool Renderer::createShadowMapRenderPass() { - try { - std::cout << "Creating shadow map render pass..." << std::endl; - - // We'll use dynamic rendering instead of traditional render passes - // This is more flexible and matches our existing rendering approach - - std::cout << "Shadow map render pass created (using dynamic rendering)" << std::endl; - return true; - - } catch (const std::exception& e) { - std::cerr << "Failed to create shadow map render pass: " << e.what() << std::endl; - return false; - } -} - -bool Renderer::createShadowMapFramebuffers() { - try { - std::cout << "Creating shadow map framebuffers..." << std::endl; - - // With dynamic rendering, we don't need traditional framebuffers - // The shadow map images will be used directly in dynamic rendering - - std::cout << "Shadow map framebuffers created (using dynamic rendering)" << std::endl; - return true; - - } catch (const std::exception& e) { - std::cerr << "Failed to create shadow map framebuffers: " << e.what() << std::endl; - return false; - } -} - -bool Renderer::createShadowMapDescriptorSetLayout() { - try { - std::cout << "Creating shadow map descriptor set layout..." << std::endl; - - // We need to update the existing PBR descriptor set layout to include shadow maps - // This will be done by modifying the createPBRDescriptorSetLayout method - - std::cout << "Shadow map descriptor set layout will be integrated with PBR layout" << std::endl; - return true; - - } catch (const std::exception& e) { - std::cerr << "Failed to create shadow map descriptor set layout: " << e.what() << std::endl; - return false; - } -} - -void Renderer::renderShadowMaps(const std::vector& entities, const std::vector& lights) { - // This method will render the scene from each light's perspective to generate shadow maps - // Implementation will be added after basic shadow map creation is working - std::cout << "Shadow map rendering not yet implemented" << std::endl; -} - -void Renderer::updateShadowMapUniforms(uint32_t lightIndex, const ExtractedLight& light) { - // This method will calculate and update the light space matrix for a specific light - // Implementation will be added after basic shadow map creation is working - std::cout << "Shadow map uniform updates not yet implemented" << std::endl; -} diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index ed847980..3e801174 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -22,14 +22,16 @@ struct VSOutput { }; // Light data structure for storage buffer +// Explicit offsets ensure exact match with CPU-side layout struct LightData { - float4 position; // Light position (w component 1 for directional) - float4 color; // Light color and intensity - float4x4 lightSpaceMatrix; // Light space matrix for shadow mapping - int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive - float range; // Light range - float innerConeAngle; // For spot lights - float outerConeAngle; // For spot lights + [[vk::offset(0)]] float4 position; // Directional: direction (w=0); Point/Spot/Emissive: world position (w=1) + [[vk::offset(16)]] float4 color; // Light color and intensity + // Match GLM column-major matrices in CPU + [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; // Light space matrix for shadow mapping + [[vk::offset(96)]] int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive + [[vk::offset(100)]] float range; // Light range + [[vk::offset(104)]] float innerConeAngle;// For spot lights + [[vk::offset(108)]] float outerConeAngle;// For spot lights }; // Uniform buffer (now without fixed light arrays) @@ -43,9 +45,9 @@ struct UniformBufferObject { float prefilteredCubeMipLevels; float scaleIBLAmbient; int lightCount; // Number of active lights (dynamic) - int shadowMapCount; // Number of active shadow maps (dynamic) - float shadowBias; // Shadow bias to prevent shadow acne - float padding; // Padding for alignment + int padding0; // Padding for alignment + float padding1; // Padding for alignment + float padding2; // Padding for alignment }; // Push constants for material properties @@ -74,8 +76,7 @@ static const float PI = 3.14159265359; [[vk::binding(3, 0)]] Sampler2D normalMap; [[vk::binding(4, 0)]] Sampler2D occlusionMap; [[vk::binding(5, 0)]] Sampler2D emissiveMap; -[[vk::binding(6, 0)]] Sampler2D shadowMaps[16]; // Array of individual shadow maps for lights -[[vk::binding(7, 0)]] StructuredBuffer lightBuffer; // Dynamic light storage buffer +[[vk::binding(6, 0)]] StructuredBuffer lightBuffer; // Dynamic light storage buffer [[vk::push_constant]] PushConstants material; @@ -106,49 +107,6 @@ float3 FresnelSchlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } -// Shadow mapping functions -float calculateShadow(float4 fragPosLightSpace, int shadowMapIndex, float3 normal, float3 lightDir) { - // Add bounds checking to prevent accessing invalid shadow map indices - if (shadowMapIndex < 0 || shadowMapIndex >= 16) { - return 0.0; // No shadow for invalid indices - } - - // Perform perspective divide - float3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; - - // Transform to [0,1] range - projCoords = projCoords * 0.5 + 0.5; - - // Check if fragment is outside light's frustum - if (projCoords.z > 1.0 || projCoords.x < 0.0 || projCoords.x > 1.0 || - projCoords.y < 0.0 || projCoords.y > 1.0) { - return 0.0; // No shadow outside frustum - } - - // Calculate bias to prevent shadow acne - float bias = max(ubo.shadowBias * (1.0 - dot(normal, lightDir)), ubo.shadowBias * 0.1); - - // Get closest depth value from light's perspective - float closestDepth = shadowMaps[shadowMapIndex].Sample(projCoords.xy).r; - - // Get depth of current fragment from light's perspective - float currentDepth = projCoords.z; - - // PCF (Percentage Closer Filtering) for smooth shadows - float shadow = 0.0; - float2 texelSize = 1.0 / float2(2048.0, 2048.0); // Shadow map resolution - - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - float pcfDepth = shadowMaps[shadowMapIndex].Sample(projCoords.xy + float2(x, y) * texelSize).r; - shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; - } - } - shadow /= 9.0; // Average the 9 samples - - return shadow; -} - // Vertex shader entry point [[shader("vertex")]] VSOutput VSMain(VSInput input) @@ -217,15 +175,14 @@ float4 PSMain(VSOutput input) : SV_TARGET // Calculate lighting for each light (dynamic count - no limit) for (int i = 0; i < ubo.lightCount; i++) { LightData light = lightBuffer[i]; - float3 lightPos = mul(light.lightSpaceMatrix, float4(input.WorldPos, 1.0)).xyz; - float3 lightVec = light.position.xyz; + float3 lightVec = light.position.xyz; // w=0 indicates direction (directional), w=1 indicates position (point/spot/emissive) float3 lightColor = light.color.rgb; // Handle emissive lights differently if (light.lightType == 3) { // Emissive light // Treat emissive like a positional contributor from its stored position - float3 L = normalize(lightPos - input.WorldPos); - float distance = length(lightPos - input.WorldPos); + float3 L = normalize(lightVec - input.WorldPos); + float distance = length(lightVec - input.WorldPos); float attenuation = 1.0 / (distance * distance); float3 radiance = lightColor * attenuation; @@ -236,12 +193,6 @@ float4 PSMain(VSOutput input) : SV_TARGET float NdotH = max(dot(N, H), 0.0); float HdotV = max(dot(H, V), 0.0); - float shadow = 0.0; - if (i < ubo.shadowMapCount) { - float4 fragPosLightSpace = mul(light.lightSpaceMatrix, float4(input.WorldPos, 1.0)); - shadow = calculateShadow(fragPosLightSpace, i, N, L); - } - float D = DistributionGGX(NdotH, roughness); float G = GeometrySmith(NdotV, NdotL, roughness); float3 F = FresnelSchlick(HdotV, F0); @@ -254,8 +205,7 @@ float4 PSMain(VSOutput input) : SV_TARGET float3 kD = float3(1.0, 1.0, 1.0) - kS; kD *= 1.0 - metallic; - float shadowFactor = 1.0 - shadow; - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL * shadowFactor; + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; } else if (light.lightType == 1) { // Directional light // For directional lights, position field stores direction; use no distance attenuation @@ -269,12 +219,6 @@ float4 PSMain(VSOutput input) : SV_TARGET float NdotH = max(dot(N, H), 0.0); float HdotV = max(dot(H, V), 0.0); - float shadow = 0.0; - if (i < ubo.shadowMapCount) { - float4 fragPosLightSpace = mul(light.lightSpaceMatrix, float4(input.WorldPos, 1.0)); - shadow = calculateShadow(fragPosLightSpace, i, N, L); - } - float D = DistributionGGX(NdotH, roughness); float G = GeometrySmith(NdotV, NdotL, roughness); float3 F = FresnelSchlick(HdotV, F0); @@ -287,12 +231,11 @@ float4 PSMain(VSOutput input) : SV_TARGET float3 kD = float3(1.0, 1.0, 1.0) - kS; kD *= 1.0 - metallic; - float shadowFactor = 1.0 - shadow; - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL * shadowFactor; + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; } else { // Point/Spot lights - float3 L = normalize(lightPos - input.WorldPos); - float distance = length(lightPos - input.WorldPos); + float3 L = normalize(lightVec - input.WorldPos); + float distance = length(lightVec - input.WorldPos); float attenuation = 1.0 / (distance * distance); float3 radiance = lightColor * attenuation; @@ -303,12 +246,6 @@ float4 PSMain(VSOutput input) : SV_TARGET float NdotH = max(dot(N, H), 0.0); float HdotV = max(dot(H, V), 0.0); - float shadow = 0.0; - if (i < ubo.shadowMapCount) { - float4 fragPosLightSpace = mul(light.lightSpaceMatrix, float4(input.WorldPos, 1.0)); - shadow = calculateShadow(fragPosLightSpace, i, N, L); - } - float D = DistributionGGX(NdotH, roughness); float G = GeometrySmith(NdotV, NdotL, roughness); float3 F = FresnelSchlick(HdotV, F0); @@ -321,8 +258,7 @@ float4 PSMain(VSOutput input) : SV_TARGET float3 kD = float3(1.0, 1.0, 1.0) - kS; kD *= 1.0 - metallic; - float shadowFactor = 1.0 - shadow; - Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL * shadowFactor; + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; } } From e57bc3d00cba69a79128763998ee1c8845900122 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 12 Aug 2025 23:22:08 -0700 Subject: [PATCH 051/102] Fix the string lights. --- .../simple_engine/renderer_rendering.cpp | 26 ++++++------------- .../simple_engine/renderer_resources.cpp | 1 + attachments/simple_engine/shaders/pbr.slang | 3 ++- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 3d4bd2ee..99fda207 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -695,25 +695,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {}); - // Define PBR push constants structure matching the shader - struct PushConstants { - glm::vec4 baseColorFactor; - float metallicFactor; - float roughnessFactor; - int baseColorTextureSet; - int physicalDescriptorTextureSet; - int normalTextureSet; - int occlusionTextureSet; - int emissiveTextureSet; - float alphaMask; - float alphaMaskCutoff; - glm::vec3 emissiveFactor; // Add emissive factor for HDR emissive sources - float emissiveStrength; // KHR_materials_emissive_strength extension - }; // Set PBR material properties using push constants if (usePBR) { - PushConstants pushConstants{}; + MaterialProperties pushConstants{}; // Try to get material properties for this specific entity if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { @@ -758,7 +743,12 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam pushConstants.physicalDescriptorTextureSet = 0; pushConstants.normalTextureSet = 0; pushConstants.occlusionTextureSet = 0; - pushConstants.emissiveTextureSet = 0; + // For emissive: indicate absence with -1 so shader uses factor-only emissive + int emissiveSet = -1; + if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) { + emissiveSet = 0; + } + pushConstants.emissiveTextureSet = emissiveSet; pushConstants.alphaMask = 0.0f; pushConstants.alphaMaskCutoff = 0.5f; @@ -767,7 +757,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* cam **currentLayout, vk::ShaderStageFlagBits::eFragment, 0, - vk::ArrayProxy(sizeof(PushConstants), reinterpret_cast(&pushConstants)) + vk::ArrayProxy(sizeof(MaterialProperties), reinterpret_cast(&pushConstants)) ); } diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 36c34e34..61b0ba21 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1356,6 +1356,7 @@ bool Renderer::preAllocateEntityResources(Entity* entity) { return false; } + // 3. Pre-allocate BOTH basic and PBR descriptor sets std::string texturePath = meshComponent->GetTexturePath(); // Fallback: if legacy texturePath is empty, use PBR baseColor texture diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 3e801174..e64e671a 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -148,7 +148,8 @@ float4 PSMain(VSOutput input) : SV_TARGET float metallic = metallicRoughness.x * material.metallicFactor; float roughness = metallicRoughness.y * material.roughnessFactor; float ao = occlusionMap.Sample(uv).r; - float3 emissive = emissiveMap.Sample(uv).rgb * material.emissiveFactor * material.emissiveStrength; + float3 emissiveTex = (material.emissiveTextureSet < 0) ? float3(1.0, 1.0, 1.0) : emissiveMap.Sample(uv).rgb; + float3 emissive = emissiveTex * material.emissiveFactor * material.emissiveStrength; // Calculate normal in tangent space float3 N = normalize(input.Normal); From e1d705942bd7b5306935805eb4f26906699ee579 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 11:58:34 -0700 Subject: [PATCH 052/102] committing requested changes in stages. --- .../Appendix/appendix.adoc | 9 +- .../01_introduction.adoc | 10 +- .../02_math_foundations.adoc | 18 +- .../03_camera_implementation.adoc | 15 +- .../03_transformation_matrices.adoc | 10 +- .../04_camera_implementation.adoc | 380 ++++++++++++++---- .../04_transformation_matrices.adoc | 9 +- .../05_vulkan_integration.adoc | 22 +- .../Camera_Transformations/06_conclusion.adoc | 9 +- .../Camera_Transformations/index.adoc | 9 +- 10 files changed, 323 insertions(+), 168 deletions(-) diff --git a/en/Building_a_Simple_Engine/Appendix/appendix.adoc b/en/Building_a_Simple_Engine/Appendix/appendix.adoc index 38c6ce1c..c0f3b8f5 100644 --- a/en/Building_a_Simple_Engine/Appendix/appendix.adoc +++ b/en/Building_a_Simple_Engine/Appendix/appendix.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Appendix: -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Detailed Architectural Patterns @@ -18,7 +11,7 @@ This appendix provides in-depth information about common architectural patterns One of the most fundamental architectural patterns is the layered architecture, where the system is divided into distinct layers, each with a specific responsibility. -image::../../../images/layered_architecture_diagram.svg[Layered Architecture Diagram, width=600] +image::../../../images/layered_architecture_diagram.png[Layered Architecture Diagram, width=600] ==== Typical Layers in a Rendering Engine diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc index ee450bb1..e0674b62 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc @@ -1,14 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ - == Introduction Welcome to the "Camera & Transformations" chapter of our "Building a Simple Engine" series! In this chapter, we'll dive into the essential mathematics and techniques needed to implement a 3D camera system in Vulkan. diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc index 11b0c2b8..f217cb6e 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc @@ -1,12 +1,6 @@ :pp: {plus}{plus} = Camera & Transformations: Mathematical Foundations -:doctype: book -:sectnums: -:sectnumlevels: 4 -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Mathematical Foundations for 3D Graphics @@ -22,6 +16,7 @@ Vectors are fundamental to 3D graphics as they represent positions, directions, ==== Why Vectors Matter in Graphics In our camera system, vectors serve several critical purposes: + * The camera's position is represented as a 3D vector * The camera's viewing direction is a 3D vector * The "up" direction that orients the camera is also a vector @@ -39,13 +34,13 @@ In our camera system, vectors serve several critical purposes: ==== The Right-Hand Rule -The right-hand rule is a convention used in 3D graphics and mathematics to determine the orientation of coordinate systems and the direction of cross products. +The right-hand rule is a convention used in 3D graphics and mathematics to determine the orientation of coordinate systems and the direction of cross-products. * *For Cross Products*: When calculating A × B: 1. Point your right hand's index finger in the direction of vector A 2. Point your middle finger in the direction of vector B (perpendicular to A) - 3. Your thumb now points in the direction of the resulting cross product + 3. Your thumb now points in the direction of the resulting cross-product * *For Coordinate Systems*: In a right-handed coordinate system: @@ -72,7 +67,7 @@ glm::vec3 negativeZ = glm::cross(yAxis, xAxis); // Points backward (negative Z) - Applications: Generating the camera's "right" vector from "forward" and "up" vectors - The direction follows the right-hand rule (explained above) -* *Normalization*: Preserves direction while setting length to 1 +* *Normalization*: Preserves the direction while setting length to 1 - Applications: Ensuring consistent movement speed regardless of direction [source,cpp] @@ -104,6 +99,7 @@ Matrices are used to represent transformations in 3D space. In Vulkan and other ==== Why We Use 4×4 Matrices Even though we work in 3D space, we use 4×4 matrices because: + 1. They allow us to represent translation (movement) along with rotation and scaling 2. They can be combined (multiplied) to create complex transformations 3. They work with homogeneous coordinates (x, y, z, w) which are required for perspective projection @@ -120,7 +116,7 @@ Even though we work in 3D space, we use 4×4 matrices because: - Less commonly used for cameras, but important for objects in the scene * *Model Matrix*: Combines transformations to position an object in world space - - Positions objects relative to the world origin + - Positions the objects relative to the world origin * *View Matrix*: Transforms world space to camera space - Essentially positions the world relative to the camera @@ -174,7 +170,7 @@ The following diagram illustrates the difference between correct and incorrect m .Matrix Transformation Order Comparison image::../../../images/matrix-order-comparison.svg[Matrix Order Comparison showing correct T×R×S vs incorrect R×T×S transformation sequences] -==== Row-Major vs Column-Major Representation +==== Row-Major vs. Column-Major Representation When working with matrices in graphics programming, it's important to understand the difference between row-major and column-major representations: diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc index 9763acbd..d54bf086 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Camera Implementation -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Camera Implementation @@ -122,7 +115,7 @@ void Camera::processKeyboard(CameraMovement direction, float deltaTime) { ==== Handling Input Events -The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW (a common windowing library used with Vulkan): +The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW, (a common windowing library used with Vulkan): [source,cpp] ---- @@ -353,7 +346,7 @@ This implementation: ==== Occlusion Avoidance -One of the most challenging aspects of a third-person camera is handling occlusion - when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: +One of the most challenging aspects of a third-person camera is handling occlusion—when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: [source,cpp] ---- @@ -398,7 +391,7 @@ When implementing occlusion avoidance, be mindful of performance: * *Use simplified collision geometry*: For raycasting, use simpler collision shapes than your rendering geometry * *Limit the frequency of occlusion checks*: You may not need to check every frame on slower devices -* *Consider spatial partitioning*: Use structures like octrees to accelerate raycasts by quickly eliminating objects that can't possibly intersect with the ray +* *Consider spatial partitioning*: Use structures like octrees to speed up raycasts by quickly eliminating objects that can't possibly intersect with the ray * *Optimize for mobile platforms*: For performance-constrained devices, consider simplifying the occlusion algorithm or reducing its precision ==== Implementing Orbit Controls diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc index 1d66ad2e..f1cd791a 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Transformation Matrices -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Transformation Matrices @@ -92,6 +85,7 @@ glm::mat4 createPerspectiveMatrix( ---- Parameters: + * `fovY`: Field of view angle in degrees (vertical) * `aspectRatio`: Width divided by height of the viewport * `nearPlane`: Distance to the near clipping plane diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc index fdae12da..d729a9c3 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Camera Implementation -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Camera Implementation @@ -26,62 +19,128 @@ For our implementation, we'll focus on a versatile camera that can be configured === Camera Class Design -Let's design a camera class that encapsulates the necessary functionality: +Our camera system is built around a Camera class that manages 3D navigation and view generation. Let's break down the implementation into logical sections to understand both the technical details and design decisions behind each component. + +=== Camera Architecture: Core Data Members and Spatial Representation + +First, we establish the fundamental data structures that represent the camera's position, orientation, and coordinate system within 3D space. [source,cpp] ---- class Camera { private: - // Camera position and orientation - glm::vec3 position; - glm::vec3 front; - glm::vec3 up; - glm::vec3 right; - glm::vec3 worldUp; - - // Euler angles - float yaw; - float pitch; - - // Camera options - float movementSpeed; - float mouseSensitivity; - float zoom; - - // Update camera vectors based on Euler angles + // Spatial positioning and orientation vectors + // These form the camera's local coordinate system in world space + glm::vec3 position; // Camera's location in world coordinates + glm::vec3 front; // Forward direction (where camera is looking) + glm::vec3 up; // Camera's local up direction (for roll control) + glm::vec3 right; // Camera's local right direction (perpendicular to front and up) + glm::vec3 worldUp; // Global up vector reference (typically Y-axis) +---- + +The spatial representation uses a right-handed coordinate system where the camera maintains its own local coordinate frame within the world space. The `position` vector defines where the camera exists, while `front`, `up`, and `right` vectors form an orthonormal basis that defines the camera's orientation. This approach provides intuitive control where moving along the `front` vector moves the camera forward, `right` moves sideways, and `up` moves vertically relative to the camera's current orientation. + +The `worldUp` vector serves as a reference point for maintaining proper orientation, typically pointing along the world's Y-axis. This reference prevents the camera from becoming disoriented during complex rotations and ensures that operations like "level the horizon" have a consistent reference point. + +=== Camera Architecture: Euler Angle Representation and Control Parameters + +Next, we define how rotations are represented and controlled, using Euler angles for intuitive user input while managing the mathematical complexities internally. + +[source,cpp] +---- + // Rotation representation using Euler angles + // Provides intuitive control while managing gimbal lock and other rotation complexities + float yaw; // Horizontal rotation around the world up-axis (left-right looking) + float pitch; // Vertical rotation around the camera's right axis (up-down looking) + + // User interaction and behavior parameters + // These control how the camera responds to input and environmental factors + float movementSpeed; // Units per second for translation movement + float mouseSensitivity; // Multiplier for mouse input to rotation angle conversion + float zoom; // Field of view control for perspective projection +---- + +Euler angles provide an intuitive interface for camera rotation that maps naturally to user input devices. Yaw controls horizontal rotation (looking left-right), while pitch controls vertical rotation (looking up-down). We deliberately avoid roll for most applications as it can be disorienting for users, though the system could be extended to support it. + +The parameter system allows fine-tuning of camera behavior for different use cases. Movement speed can be adjusted for different scene scales, mouse sensitivity can accommodate user preferences and different input devices, and zoom provides dynamic field-of-view control for gameplay or cinematic effects. + +=== Camera Architecture: Internal Methods and State Management + +Next, we define the internal methods responsible for maintaining mathematical consistency and updating the camera's coordinate system when rotations change. + +[source,cpp] +---- + // Internal coordinate system maintenance + // Ensures mathematical consistency when orientation changes occur void updateCameraVectors(); public: - // Constructor with default values +---- + +The `updateCameraVectors` method serves as the mathematical foundation of the camera system, recalculating the `front`, `right`, and `up` vectors whenever the Euler angles change. This process involves trigonometric calculations that convert the intuitive Euler angle representation into the orthonormal vector basis required for matrix operations and movement calculations. + +This approach separates the user-friendly angle interface from the computationally efficient vector operations, allowing the camera to present simple controls while maintaining the mathematical rigor required for accurate 3D transformations. + +=== Camera Architecture: Public Interface and Constructor Design + +Next, we establish the public interface that external code uses to create, configure, and interact with camera instances. + +[source,cpp] +---- + // Constructor with sensible defaults for common use cases + // Provides flexibility while ensuring the camera starts in a predictable state Camera( - glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), - glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), - float yaw = -90.0f, - float pitch = 0.0f + glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), // Start at world origin + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), // Y-axis as world up + float yaw = -90.0f, // Look along negative Z-axis (OpenGL convention) + float pitch = 0.0f // Level horizon ); +---- - // Get view matrix - glm::mat4 getViewMatrix() const; +The constructor design reflects common 3D graphics conventions and practical defaults. The default position at the origin provides a predictable starting point, while the Y-axis world up aligns with the standard mathematical coordinate system. The initial yaw of -90° follows OpenGL conventions where the default view looks down the negative Z-axis, creating a right-handed coordinate system that feels natural to users. - // Get projection matrix +The parameter defaults eliminate the need for complex initialization in simple use cases while still allowing full customization when needed for specialized applications. + +=== Camera Architecture: Matrix Generation and Geometric Transformation Interface + +Now we define the core mathematical interface that transforms the camera's spatial representation into the matrices required by graphics pipelines. + +[source,cpp] +---- + // Matrix generation for graphics pipeline integration + // These methods bridge between the camera's spatial representation and GPU requirements + glm::mat4 getViewMatrix() const; glm::mat4 getProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 100.0f) const; +---- - // Process keyboard input for camera movement - void processKeyboard(CameraMovement direction, float deltaTime); +The matrix generation methods serve as the critical bridge between our intuitive camera representation and the mathematical requirements of 3D graphics pipelines. The view matrix transforms world coordinates into camera space, effectively positioning the world relative to the camera's viewpoint. The projection matrix then transforms camera space into clip space, handling perspective effects and preparing coordinates for rasterization. - // Process mouse movement for camera rotation - void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); +The separation of view and projection matrices follows standard graphics pipeline architecture, allowing independent control over camera positioning and perspective characteristics. This design enables techniques like changing field-of-view for zoom effects without recalculating the camera's spatial relationships. - // Process mouse scroll for zoom - void processMouseScroll(float yOffset); +=== Camera Architecture: Input Processing and User Interaction - // Getters for camera properties +Finally, let's define how the camera responds to various forms of user input, providing the interface between human interaction and camera movement. + +[source,cpp] +---- + // Input processing methods for different interaction modalities + // Each method handles a specific type of user input with appropriate transformations + void processKeyboard(CameraMovement direction, float deltaTime); // Keyboard-based translation + void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); // Mouse-based rotation + void processMouseScroll(float yOffset); // Scroll-based zoom control + + // Property access methods for external systems + // Provide controlled access to internal state without exposing implementation details glm::vec3 getPosition() const { return position; } glm::vec3 getFront() const { return front; } float getZoom() const { return zoom; } }; ---- +The input processing architecture recognizes that different input modalities serve different purposes in camera control. Keyboard input typically handles discrete directional movement, mouse movement provides continuous rotation control, and scroll wheels offer intuitive zoom adjustment. Each method is designed to handle its specific input type with appropriate mathematical transformations and timing considerations. + +The getter methods provide controlled access to internal state, allowing external systems (like audio systems that need listener position, or culling systems that need view direction) to access camera properties without exposing the internal implementation details or allowing uncontrolled modification of the camera's state. + === Camera Movement We'll define an enum for camera movement directions: @@ -122,64 +181,126 @@ void Camera::processKeyboard(CameraMovement direction, float deltaTime) { ==== Handling Input Events -The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW (a common windowing library used with Vulkan): +The camera class provides methods to process input, but integrating these with your application's input system requires careful consideration of different input modalities and their unique characteristics. Let's break down the input handling implementation to demonstrate both the technical integration and the design principles behind effective camera controls. + +=== Input Integration: Keyboard Input Processing and Movement Translation + +First, we handle discrete directional input from keyboards, translating key presses into camera movement commands with proper frame-rate independence. [source,cpp] ---- -// In your application's input handling function +// Keyboard input processing for camera translation +// Handles discrete directional commands with frame-rate independent timing void processInput(GLFWwindow* window, Camera& camera, float deltaTime) { - // Keyboard input for camera movement + // WASD movement scheme following standard FPS conventions + // Each key press translates to a specific directional movement relative to camera orientation if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); // Move forward along camera's front vector if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); // Move backward opposite to front vector if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::LEFT, deltaTime); + camera.processKeyboard(CameraMovement::LEFT, deltaTime); // Strafe left along camera's right vector if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); // Strafe right along camera's right vector + + // Vertical movement controls for 3D navigation + // Space and Control provide intuitive up/down movement if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::UP, deltaTime); + camera.processKeyboard(CameraMovement::UP, deltaTime); // Move up along camera's up vector if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) - camera.processKeyboard(CameraMovement::DOWN, deltaTime); + camera.processKeyboard(CameraMovement::DOWN, deltaTime); // Move down opposite to up vector } +---- + +The keyboard input processing follows established conventions from first-person games, where WASD keys control horizontal movement and Space/Control handle vertical movement. This mapping feels intuitive to users and provides complete 6-degrees-of-freedom movement control. The frame-rate independence achieved through deltaTime ensures consistent movement speed regardless of rendering performance, which is crucial for predictable user experience across different hardware configurations. + +Each movement command uses the camera's local coordinate system rather than world coordinates. Meaning "forward" always moves in the direction the camera is facing, "right" moves perpendicular to the view direction, and "up" moves along the camera's local vertical axis. This approach provides intuitive controls that respond naturally to camera orientation changes. -// Mouse callback function for camera rotation +=== Input Integration: Mouse Movement Processing and Rotation State Management + +Now, let's handle continuous mouse input for camera rotation, managing state persistence and coordinate system conversions for smooth camera control. + +[source,cpp] +---- +// Mouse movement callback for continuous camera rotation +// Manages state persistence and coordinate transformations for smooth rotation control void mouseCallback(GLFWwindow* window, double xpos, double ypos) { - static bool firstMouse = true; - static float lastX = 0.0f, lastY = 0.0f; + // State persistence for calculating movement deltas + // Static variables maintain state between callback invocations + static bool firstMouse = true; // Flag to handle initial mouse position + static float lastX = 0.0f, lastY = 0.0f; // Previous mouse position for delta calculation + // Handle initial mouse position to prevent sudden camera jumps + // First callback provides absolute position, not relative movement if (firstMouse) { - lastX = xpos; + lastX = xpos; // Initialize previous position lastY = ypos; - firstMouse = false; + firstMouse = false; // Disable special handling for subsequent calls } - float xoffset = xpos - lastX; - float yoffset = lastY - ypos; // Reversed: y ranges bottom to top + // Calculate mouse movement deltas since last callback + // These deltas represent the amount and direction of mouse movement + float xoffset = xpos - lastX; // Horizontal movement (left-right) + float yoffset = lastY - ypos; // Vertical movement (inverted: screen Y increases downward, camera pitch increases upward) + // Update state for next callback iteration lastX = xpos; lastY = ypos; - // Pass the mouse movement to the camera + // Convert mouse movement to camera rotation + // Delta values drive continuous camera orientation changes camera.processMouseMovement(xoffset, yoffset); } +---- + +The mouse callback demonstrates the complexities of handling continuous input in event-driven systems. The static variables maintain state between callback invocations, which is necessary because mouse movement is reported as absolute positions rather than relative deltas. The first-mouse handling prevents jarring camera jumps when the mouse cursor is first captured. + +The Y-axis inversion (`lastY - ypos`) addresses the coordinate system mismatch between screen space (where Y increases downward) and camera space (where positive pitch looks upward). This inversion ensures that moving the mouse upward rotates the camera to look up, matching user expectations from other 3D applications. -// Scroll callback for zoom +=== Input Integration: Scroll Input Processing and Zoom Control + +Next, let's work on the scroll-wheel input to give us zoom control, providing a simple interface for field-of-view adjustments that feel natural to users. + +[source,cpp] +---- +// Scroll wheel callback for zoom control +// Provides intuitive field-of-view adjustment through scroll wheel interaction void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + // Direct scroll-to-zoom mapping + // Positive yoffset (scroll up) typically zooms in, negative (scroll down) zooms out camera.processMouseScroll(yoffset); } +---- + +The scroll callback maintains simplicity by directly passing the scroll delta to the camera's zoom processing method. This design delegates the mathematical details of zoom control to the camera class while providing a clean interface for scroll wheel events. The scroll direction convention (positive for zoom in, negative for zoom out) follows standard user interface patterns. + +=== Input Integration: System Integration and Input Mode Configuration -// Setting up the callbacks in your initialization code +Finally, we establish the integration between the input callbacks and the windowing system, configuring mouse capture and callback registration for complete camera control. + +[source,cpp] +---- +// Input system initialization and callback registration +// Establishes the connection between windowing system and camera control callbacks void setupInputCallbacks(GLFWwindow* window) { - glfwSetCursorPosCallback(window, mouseCallback); - glfwSetScrollCallback(window, scrollCallback); - glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // Capture mouse + // Register callback functions with the windowing system + // These establish the event-driven connection between hardware input and camera control + glfwSetCursorPosCallback(window, mouseCallback); // Connect mouse movement to camera rotation + glfwSetScrollCallback(window, scrollCallback); // Connect scroll wheel to camera zoom + + // Configure mouse capture mode for first-person camera behavior + // Disabling the cursor provides continuous mouse input without cursor interference + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); } ---- +The system integration demonstrates how camera controls integrate with the broader application architecture. The callback registration creates the event-driven connection between hardware input and camera responses, while the cursor disabling provides the seamless mouse control expected in 3D applications. + +The `GLFW_CURSOR_DISABLED` mode captures the mouse cursor, allowing unlimited mouse movement without the cursor hitting screen boundaries. This configuration is essential for first-person camera controls where users expect to be able to turn the camera continuously in any direction without cursor limitations. + [NOTE] ==== -The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. +The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the link:https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. ==== === Camera Rotation @@ -263,53 +384,134 @@ A third-person camera typically needs to: 3. Avoid being occluded by objects in the environment 4. Provide smooth transitions during movement and rotation -Let's extend our camera class to support these features: +Let's extend our camera class to support these features by building a specialized ThirdPersonCamera that addresses the unique challenges of the character-following camera systems. + +=== Third-Person Camera Architecture: Target Tracking and Spatial Relationship Management + +What good is a camera if we can't use it to target looking at things? Maybe we also want characters to look at each other or to have them look at the camera. Let's start work on this by figuring out how a 'lookat' system would work and how a camera would track a target. + +The getter methods provide controlled access to internal state, allowing external systems (like audio systems that need listener position, or culling systems that need a view direction) to query the camera without tightly coupling to the implementation. This keeps the camera easy to maintain and extend as features are added. + +==== Look-At Basics: Pointing the Camera at Something + +Before we automate camera behaviors, let’s build an intuition for “look-at.” The idea is simple: given we know where the camera is: "the eye," we also know what point it should face: "the target," and we also know which way is "up." We want to use that information to construct an orientation that makes the camera face the target while keeping the horizon stable. + +Think of it like lining up a real camera: + +- Eye: “Where am I standing?” +- Target: “What am I framing in the center of the viewfinder?” +- Up: “Which direction should the top of the frame point (so the picture isn’t tilted)?” + +Thus, when we get to the output of "look at," we will have a view orientation. We usually for convenience will use an affine matrix, but it is only an orientation. After all, rotating to "look at" something shouldn't involve translating to a new position; so the eye will maintain the position throughout our look-at code. + +Key takeaways: + +- “Look-at” defines an orientation, not a position. The position comes from the eye; look-at figures out the directions (forward/right/up) from eye→target and the chosen up. +- The up direction should not be parallel to the eye→target direction. If they’re nearly aligned, the camera won’t know how to keep the horizon level (it can “roll unpredictably.”) +- You can use look-at for both cameras and objects. Characters can face each other, or you can point a spotlight or turret at a target with the same concept. + +In the next section, we’ll take this one-off “point at a target” idea and turn it into a behavior: smooth, continuous camera target tracking that follows moving subjects without jitter or sudden snaps. + +=== Implementation for camera target relationship + +First, establish the fundamental relationship between the camera and its target, managing the spatial tracking information that drives all third-person camera behaviors. [source,cpp] ---- class ThirdPersonCamera : public Camera { private: - // Target (character) properties - glm::vec3 targetPosition; - glm::vec3 targetForward; + // Target entity tracking and spatial relationship data + // These properties define the relationship between camera and the character being followed + glm::vec3 targetPosition; // Current world position of the target character + glm::vec3 targetForward; // Target's forward direction vector for contextual camera positioning +---- + +The target tracking system forms the foundation of third-person camera behavior by maintaining a continuous connection between the camera and the character being followed. The `targetPosition` provides the spatial anchor that the camera revolves around, while `targetForward` enables context-aware camera positioning that can anticipate where the character is moving or looking. + +This approach allows the camera to make intelligent positioning decisions based on the character's state and orientation, creating more dynamic and responsive camera behavior than simple fixed-offset following. - // Camera configuration - float followDistance; - float followHeight; - float followSmoothness; +=== Third-Person Camera Architecture: Behavioral Configuration and Control Parameters - // Occlusion avoidance - float minDistance; - float raycastDistance; +Now let's work on the parameters that control how the camera behaves in relation to its target, providing artistic and gameplay control over the camera's characteristics. - // Internal state - glm::vec3 desiredPosition; - glm::vec3 smoothDampVelocity; +[source,cpp] +---- + // Camera behavior configuration parameters + // These values control the aesthetic and functional characteristics of camera following + float followDistance; // Desired distance from target (affects intimacy and field of view) + float followHeight; // Height offset above target (provides better scene visibility) + float followSmoothness; // Interpolation factor for smooth camera transitions (0 = instant, 1 = never) +---- + +The behavioral parameters provide artistic control over the camera's personality and functional characteristics. Follow distance affects both the visual intimacy with the character and the amount of surrounding environment visible in the frame. Height offset ensures the camera provides good visibility of both the character and the surrounding terrain or obstacles. + +The smoothness parameter controls the camera's responsiveness to target movement, allowing designers to balance between immediate response, (which can feel jerky,) and smooth motion (which can feel laggy). This parameter is crucial for creating camera behavior that feels natural and responsive to different gameplay situations. + +=== Third-Person Camera Architecture: Collision Detection and Occlusion Management + +Now, we have a camera system that will work in basic situations. However, let's briefly talk about the complex problem of environmental occlusion, ensuring the camera maintains visibility of the target even when obstacles interfere with the desired positioning. + +[source,cpp] +---- + // Occlusion avoidance and collision management + // These parameters control how the camera responds to environmental obstacles + float minDistance; // Minimum allowed distance from target (prevents camera from getting too close) + float raycastDistance; // Maximum distance for occlusion detection rays +---- + +The occlusion management system addresses one of the most challenging aspects of third-person camera implementation: maintaining visibility when environmental geometry interferes with the desired camera position. The minimum distance prevents the camera from getting uncomfortably close to the character during collision situations, while the raycast distance defines how far the camera looks ahead for potential occlusion issues. + +This system enables the camera to proactively respond to environmental constraints, smoothly adjusting its position to maintain optimal visibility without jarring transitions or sudden position changes that can be disorienting to players. + +=== Third-Person Camera Architecture: Internal State Management and Motion Control + +To get smooth camera motion, we need to be able to understand the FSM (Finite State Machine) design of the Camera architecture. We manage the internal computational state required for intelligent positioning decisions and to help solve smooth camera motion. + +[source,cpp] +---- + // Internal computational state for smooth motion control + // These variables manage the mathematical aspects of camera positioning and movement + glm::vec3 desiredPosition; // Target position the camera wants to reach (before collision adjustments) + glm::vec3 smoothDampVelocity; // Velocity state for smooth damping interpolation algorithms public: +---- + +The internal state management separates the desired camera behavior from the actual camera position, allowing the system to handle complex scenarios where multiple forces influence camera positioning. The desired position represents where the camera would ideally be placed based on the follow parameters, while the smooth damp velocity enables sophisticated interpolation algorithms that create natural, physics-inspired camera motion. + +This separation of concerns allows the camera system to handle conflicts between desired positioning and environmental constraints gracefully, maintaining smooth motion even when the camera must deviate significantly from its preferred location. + +=== Third-Person Camera Architecture: Public Interface and Configuration Control + +Now, let's examine the external interface that allows game code to interact with and configure the third-person camera system in a manner that can avoid tight coupling and can keep the camera as its' own module. + +[source,cpp] +---- + // Constructor with gameplay-tuned defaults + // Default values chosen for common third-person game scenarios ThirdPersonCamera( - float followDistance = 5.0f, - float followHeight = 2.0f, - float followSmoothness = 0.1f, - float minDistance = 1.0f + float followDistance = 5.0f, // Medium distance providing good character visibility and environment context + float followHeight = 2.0f, // Height above target for clear sightlines over low obstacles + float followSmoothness = 0.1f, // Moderate smoothing for responsive but stable camera motion + float minDistance = 1.0f // Minimum distance to prevent uncomfortable close-ups ); - // Update camera position based on target + // Core functionality methods for camera behavior control void updatePosition(const glm::vec3& targetPos, const glm::vec3& targetFwd, float deltaTime); - - // Handle occlusion avoidance void handleOcclusion(const Scene& scene); - - // Orbit around target void orbit(float horizontalAngle, float verticalAngle); - // Setters for camera properties + // Runtime configuration methods for dynamic camera adjustment void setFollowDistance(float distance) { followDistance = distance; } void setFollowHeight(float height) { followHeight = height; } void setFollowSmoothness(float smoothness) { followSmoothness = smoothness; } }; ---- +The public interface design balances ease of use with powerful functionality, providing sensible defaults that work well for common third-person scenarios while allowing full customization when needed. The default values are chosen based on common third-person game requirements: medium distance for good character visibility, moderate height for environmental awareness, and balanced smoothing for responsive yet stable motion. + +The method organization separates the core update functionality (which typically runs every frame) from configuration methods (which are called less frequently) and specialized behaviors like orbiting (which might be triggered by specific user input). This design makes it easy to integrate the camera into different game loop architectures while maintaining a clear separation of concerns. + ==== Character Following Algorithm The core of a third-person camera is the algorithm that positions the camera relative to the character. Here's an implementation of the `updatePosition` method: @@ -482,7 +684,7 @@ void gameLoop(float deltaTime) { [NOTE] ==== -For more advanced camera techniques, refer to the Advanced Camera Techniques section in the Appendix. +For more advanced camera techniques, refer to the Advanced Camera Techniques section in the link:../Appendix/appendix.adoc[Appendix]. ==== In the next section, we'll integrate our camera system with Vulkan to render 3D scenes. diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc index 07c9b3db..cab14251 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Transformation Matrices -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Transformation Matrices diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc index 3f7b5000..b8919669 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Vulkan Integration -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Integrating Camera with Vulkan @@ -23,6 +16,19 @@ Before we dive into the integration, let's briefly introduce the key libraries w Now that we have a camera system and understand transformation matrices, let's integrate them with our Vulkan application. We'll focus on how to set up uniform buffers for our matrices and update them each frame based on camera movement. +To keep the integration digestible, think of it in five small steps: + +* Define the UBO layout (model/view/proj) and create per-frame buffers +* Create a descriptor set layout and allocate descriptor sets for the UBO +* Write descriptor sets and persistently map the buffers for fast updates +* Update the UBO each frame from the camera (view/proj) and model transform +* Bind the descriptor set and draw using the updated matrices + +[NOTE] +==== +See link:04_transformation_matrices.adoc[Camera transformation matricies] and link:04_camera_implementation.adoc[Camera implementation] for a refresher on Matrix math. +==== + === Uniform Buffer Setup First, we need to define our uniform buffer structure: diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc index 6b635d64..34017228 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc index ecb8b843..fbc3369f 100644 --- a/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc +++ b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Camera & Transformations -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ This chapter covers the implementation of a 3D camera system and the mathematical foundations of 3D transformations in Vulkan. From 1f1707453ef4e088473cbb32d073bc34abfcb9f9 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 12:42:27 -0700 Subject: [PATCH 053/102] committing requested changes in stages. --- .../Engine_Architecture/01_introduction.adoc | 39 +- .../02_architectural_patterns.adoc | 7 - .../03_component_systems.adoc | 7 - .../04_resource_management.adoc | 351 ++++++-- .../05_rendering_pipeline.adoc | 754 ++++++++++++------ .../Engine_Architecture/06_event_systems.adoc | 34 +- .../Engine_Architecture/conclusion.adoc | 43 +- 7 files changed, 844 insertions(+), 391 deletions(-) diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc index 9ac98c9e..a3841ce0 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Introduction @@ -26,41 +19,35 @@ well-designed architecture is crucial for creating a flexible, maintainable, and === What You'll Learn -In this chapter, we'll cover: +This chapter will take you through the foundational concepts that underpin effective engine design. We'll begin by exploring architectural patterns—the proven design approaches that game and rendering engines rely on to manage complexity and enable extensibility. Understanding these patterns helps you choose the right structural approach for different engine subsystems. -* *Architectural Patterns* - Common design patterns used in game and rendering engines, including their strengths and trade-offs. +From there, we'll dive into component systems, which provide the flexibility to build modular, reusable code. You'll see how component-based architecture allows different parts of your engine to work together while remaining loosely coupled, making your codebase easier to maintain and extend. -* *Component Systems* - How to implement a flexible component-based architecture that allows for modular and reusable code. +Resource management forms another crucial pillar of engine architecture. We'll examine strategies for efficiently handling textures, models, shaders, and other assets, ensuring your engine can scale from simple scenes to complex, asset-heavy applications without performance bottlenecks. -* *Resource Management* - Strategies for efficiently managing and accessing various resources like textures, models, and shaders. +The rendering pipeline design we'll cover shows you how to create a flexible system that can accommodate various rendering techniques and effects. This foundation will serve you well as you add more advanced rendering features in later chapters. -* *Rendering Pipeline* - Designing a flexible rendering pipeline that can accommodate different rendering techniques and effects. - -* *Event Systems* - Implementing communication between different parts of your engine through event-driven architecture. +Finally, we'll implement event systems that enable clean communication between different engine components. This event-driven approach reduces tight coupling and makes your engine more maintainable as it grows in complexity. === Prerequisites -Before starting this chapter, you should have completed: - -* The main Vulkan tutorial series +This chapter builds directly on the foundation established in the main Vulkan tutorial series, so completing that series is essential. The architectural concepts we'll discuss assume you're comfortable with Vulkan's core rendering concepts and have hands-on experience implementing them. -You should also be familiar with: +Beyond Vulkan knowledge, you'll benefit from familiarity with object-oriented programming principles, as modern engine architecture relies heavily on encapsulation, inheritance, and polymorphism to manage complexity. Experience with common design patterns like Observer, Factory, and Singleton will help you recognize when and why we apply these patterns in our engine design. -* Object-oriented programming concepts -* Basic design patterns (Observer, Factory, Singleton, etc.) -* Modern C++ features (smart pointers, templates, etc.) +Modern C++ features play a crucial role in our implementation approach. Smart pointers help us manage resource lifetimes safely, templates enable flexible, reusable components, and other C++11/14/17 features allow us to write more expressive and maintainable code. If you're not comfortable with these concepts, consider reviewing them before proceeding. === Why Architecture Matters -A well-designed engine architecture provides several benefits: +The difference between a hastily assembled renderer and a well-architected engine becomes apparent as soon as you need to make changes or add features. Good architecture creates a foundation that supports your development process rather than fighting against it. -1. *Maintainability* - Clean separation of concerns makes it easier to update and fix individual components without affecting the entire system. +Maintainability emerges from clean separation of concerns—when each component has a clear, focused responsibility, you can update or fix individual pieces without worrying about cascading effects throughout the system. This becomes invaluable when debugging graphics issues or implementing new rendering techniques. -2. *Extensibility* - A modular design allows you to add new features without major refactoring. +Extensibility flows naturally from modular design. When your architecture provides clear extension points and interfaces, adding new features becomes a matter of implementing new components rather than rewriting existing systems. This allows your engine to evolve with your project's needs. -3. *Reusability* - Well-encapsulated components can be reused across different projects or parts of the same project. +Reusability multiplies your development effort. Well-encapsulated components can move between projects or serve different purposes within the same project. A thoughtfully designed material system, for example, might work equally well for both game objects and UI elements. -4. *Performance* - A thoughtful architecture can enable optimizations like multithreading, batching, and caching. +Performance opportunities often emerge from architectural decisions made early in development. Good architecture enables optimizations like multithreading (by avoiding tight coupling between systems), batching (through predictable interfaces), and caching (via clear data flow patterns). While premature optimization is dangerous, premature architecture decisions can make later optimization impossible. Let's begin our exploration of engine architecture with an overview of common architectural patterns used in modern rendering engines. diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc index ea89d335..b2e6a395 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Architectural Patterns -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Architectural Patterns diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc index a1dea87f..b77b20f4 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Component Systems -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Component Systems diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc index 6cf07e65..735a2c0b 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Resource Management -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Resource Management @@ -79,23 +72,29 @@ Using handles instead of direct pointers provides several benefits: === Basic Resource Manager -Let's implement a basic resource manager that can handle different types of resources: +Let's implement a basic resource manager that can handle different types of resources. This implementation involves several key steps that work together to provide efficient resource management for a rendering engine. + +=== Resource Manager: Base Resource Architecture and State Management + +First, we establish the fundamental infrastructure for resource management, defining how resources track their identity and loading state within the system. [source,cpp] ---- // Resource base class class Resource { private: - std::string resourceId; - bool loaded = false; + std::string resourceId; // Unique identifier for this resource within the system + bool loaded = false; // Loading state flag for resource lifecycle management public: explicit Resource(const std::string& id) : resourceId(id) {} virtual ~Resource() = default; + // Core resource identity and state access methods const std::string& GetId() const { return resourceId; } bool IsLoaded() const { return loaded; } + // Virtual interface for resource-specific loading and unloading behavior virtual bool Load() { loaded = true; return true; @@ -105,269 +104,485 @@ public: loaded = false; } }; +---- + +The Resource base class provides the foundational contract that all resource types must fulfill. The resource ID serves as a unique identifier that allows the resource manager to locate and reference specific resources without ambiguity. This string-based approach enables human-readable resource names like "main_character_texture" or "level_1_audio" while maintaining the flexibility to use file paths or other naming schemes. + +The loading state management through the boolean flag provides essential lifecycle tracking. This simple approach allows systems to quickly determine whether a resource is ready for use without expensive validation checks. The virtual loading interface enables polymorphic behavior where different resource types can implement their own specialized loading logic while presenting a consistent interface to the management system. + +=== Resource Manager: Storage Architecture and Type Safety + +Next, we implement the core storage system that organizes resources by type while maintaining type safety and efficient access patterns. +[source,cpp] +---- // Resource manager class ResourceManager { private: - // Store resources by type and ID + // Two-level storage system: organize by type first, then by unique identifier + // This approach enables type-safe resource access while maintaining efficient lookup std::unordered_map>> resources; - // Reference counts for resources + // Reference counting system for automatic resource lifecycle management + // Maps resource IDs to their current usage count for garbage collection std::unordered_map refCounts; +---- + +The storage architecture uses a sophisticated two-level mapping system that solves several critical problems in resource management. The outer map keyed by `std::type_index` ensures complete type separation, preventing name collisions between different resource types. For example, you could have both a texture named "stone" and a sound effect named "stone" without conflicts, as they're stored in separate type-specific containers. + +The inner maps provide O(1) average-case lookup performance for individual resources, which is crucial when the rendering system needs to access hundreds or thousands of resources per frame. The use of `std::shared_ptr` provides automatic memory management and enables safe sharing of resources between different systems without manual lifetime management. + +The reference counting system operates independently of the shared_ptr reference counting to provide application-level lifecycle control. This separation allows the resource manager to implement custom policies for resource retention and cleanup that go beyond simple memory management, such as keeping frequently used resources loaded even when not immediately referenced. + +=== Resource Manager: Resource Loading and Caching Logic +Then, we implement the intelligent resource loading system that handles caching, reference counting, and error recovery for efficient resource management. + +[source,cpp] +---- public: template ResourceHandle Load(const std::string& resourceId) { static_assert(std::is_base_of::value, "T must derive from Resource"); - // Check if resource already exists + // Step 3a: Check existing resource cache to avoid redundant loading auto& typeResources = resources[std::type_index(typeid(T))]; auto it = typeResources.find(resourceId); if (it != typeResources.end()) { - // Resource exists, increment reference count + // Resource exists in cache - increment reference count and return handle refCounts[resourceId]++; return ResourceHandle(resourceId, this); } - // Create and load new resource + // Step 3b: Create new resource instance and attempt loading auto resource = std::make_shared(resourceId); if (!resource->Load()) { - // Failed to load + // Loading failed - return invalid handle rather than corrupting cache return ResourceHandle(); } - // Store resource and set reference count + // Step 3c: Cache successful resource and initialize reference tracking typeResources[resourceId] = resource; refCounts[resourceId] = 1; return ResourceHandle(resourceId, this); } +---- + +The loading logic implements a sophisticated caching strategy that balances performance with memory efficiency. The cache-first approach prevents redundant I/O operations and resource processing, which can be expensive for large textures, complex meshes, or compiled shaders. This strategy is particularly important in rendering engines where the same resources may be referenced by multiple objects or systems. + +The template-based design with compile-time type checking ensures type safety while maintaining the flexibility to work with any resource type that derives from the base Resource class. The static assertion provides clear error messages during development, preventing runtime type errors that could be difficult to debug in complex rendering scenarios. + +Error handling follows the principle of graceful degradation, where loading failures return invalid handles rather than throwing exceptions or corrupting the resource cache. This approach allows rendering systems to continue operating with fallback resources or alternative rendering paths when specific assets are unavailable or corrupted. +=== Resource Manager: Resource Access and Validation Interface + +After that, we provide the interface for safely accessing cached resources with proper validation and type checking throughout the resource lifecycle. + +[source,cpp] +---- template T* GetResource(const std::string& resourceId) { + // Access type-specific resource container using compile-time type information auto& typeResources = resources[std::type_index(typeid(T))]; auto it = typeResources.find(resourceId); if (it != typeResources.end()) { + // Resource found - perform safe downcast and return typed pointer return static_cast(it->second.get()); } + // Resource not found - return null for safe handling by caller return nullptr; } template bool HasResource(const std::string& resourceId) { + // Efficient existence check without resource access overhead auto& typeResources = resources[std::type_index(typeid(T))]; return typeResources.find(resourceId) != typeResources.end(); } +---- + +The resource access interface prioritizes safety and performance in equal measure. The template-based approach ensures that clients always receive correctly typed resource pointers, eliminating the need for manual casting and reducing the potential for type-related runtime errors. The static_cast is safe because the type_index-based storage guarantees that only objects of type T are stored in each type-specific container. + +The existence check provides an efficient way to validate resource availability without the overhead of full resource access. This capability is valuable for conditional rendering logic, where systems can choose alternative rendering paths based on resource availability without triggering expensive cache misses or I/O operations. +=== Resource Manager: Reference Counting and Automatic Cleanup + +Finally, we implement intelligent resource lifecycle management through reference counting and automatic cleanup to prevent memory leaks and optimize resource utilization. + +[source,cpp] +---- void Release(const std::string& resourceId) { + // Locate reference count entry for this resource auto it = refCounts.find(resourceId); if (it != refCounts.end()) { it->second--; + // Check if resource has no remaining references if (it->second <= 0) { - // No more references, unload the resource + // Step 5a: Locate and unload the unreferenced resource across all type containers for (auto& [type, typeResources] : resources) { auto resourceIt = typeResources.find(resourceId); if (resourceIt != typeResources.end()) { - resourceIt->second->Unload(); - typeResources.erase(resourceIt); + resourceIt->second->Unload(); // Allow resource to clean up its data + typeResources.erase(resourceIt); // Remove from cache break; } } + // Step 5b: Clean up reference counting entry refCounts.erase(it); } } } void UnloadAll() { + // Emergency cleanup method for system shutdown or major state changes for (auto& [type, typeResources] : resources) { for (auto& [id, resource] : typeResources) { - resource->Unload(); + resource->Unload(); // Ensure all resources clean up properly } - typeResources.clear(); + typeResources.clear(); // Clear type-specific containers } - refCounts.clear(); + refCounts.clear(); // Reset all reference counts } }; ---- +The reference counting system provides automatic garbage collection for resources that are no longer actively used. This approach prevents memory leaks while avoiding the overhead of constantly monitoring resource usage across the entire application. The decrement-and-check pattern ensures that resources are unloaded immediately when they become unused, helping to keep memory usage optimal. + +The cleanup process is designed to be thorough and safe, ensuring that resources have the opportunity to properly release their internal data (GPU memory, file handles, etc.) before being removed from the cache. This two-phase cleanup approach prevents resource leaks and maintains system stability even under error conditions. + +The global unload functionality provides a safety valve for major state transitions like level changes or application shutdown, where you want to ensure all resources are properly cleaned up regardless of their reference counts. This capability is essential for preventing resource leaks that could accumulate over long application runs. + === Implementing Specific Resource Types -Now let's implement some specific resource types: +Now let's implement some specific resource types that demonstrate how different asset types can be integrated into our resource management system. These implementations showcase the flexibility of the base Resource interface while addressing the unique requirements of different content types. + +=== Texture Resource Implementation + +The Texture resource represents one of the most complex resource types in a rendering engine, requiring careful management of GPU memory, format conversion, and sampling parameters. Let's break this implementation into logical phases that demonstrate both the technical challenges and design solutions. + +=== Texture Resource: Resource Structure and Vulkan State Management + +First, we establish the fundamental data structures required for Vulkan texture management, including GPU resources and metadata needed for proper texture usage. [source,cpp] ---- // Texture resource class Texture : public Resource { private: - vk::Image image; - vk::DeviceMemory memory; - vk::ImageView imageView; - vk::Sampler sampler; + // Core Vulkan GPU resources for texture representation + vk::Image image; // GPU image object containing pixel data + vk::DeviceMemory memory; // GPU memory allocation backing the image + vk::ImageView imageView; // Shader-accessible view into the image + vk::Sampler sampler; // Sampling configuration (filtering, wrapping, etc.) - int width = 0; - int height = 0; - int channels = 0; + // Texture metadata for validation and debugging + int width = 0; // Image width in pixels + int height = 0; // Image height in pixels + int channels = 0; // Number of color channels (RGB=3, RGBA=4, etc.) public: explicit Texture(const std::string& id) : Resource(id) {} ~Texture() override { - Unload(); + Unload(); // Ensure proper cleanup when object is destroyed } +---- + +The Vulkan texture pipeline requires four distinct GPU objects that work together to provide complete texture functionality. The `vk::Image` represents the actual pixel data storage on the GPU, while `vk::DeviceMemory` provides the backing memory allocation. The separation between image and memory allows for advanced memory management techniques like suballocation and memory pooling. +The `vk::ImageView` serves as the interface between shaders and the image data, defining how shaders interpret the pixel format, mipmap levels, and array layers. The `vk::Sampler` encapsulates filtering and addressing modes that control how the GPU interpolates between pixels and handles texture coordinates outside the [0,1] range. This separation of concerns allows the same image to be used with different sampling configurations simultaneously. + +=== Texture Resource: Loading Pipeline and Data Acquisition + +Next, we implement the texture loading pipeline that transforms disk-based image files into GPU-ready resources through careful error handling and format conversion. + +[source,cpp] +---- bool Load() override { - // Load texture from file + // Step 2a: Construct file path using resource ID and expected format std::string filePath = "textures/" + GetId() + ".ktx"; - // Load image data using a library like stb_image or ktx + // Step 2b: Load raw image data from disk with format detection unsigned char* data = LoadImageData(filePath, &width, &height, &channels); if (!data) { - return false; + return false; // Failed to load - return failure without partial state } - // Create Vulkan image, allocate memory, and upload data + // Step 2c: Transform raw pixel data into Vulkan GPU resources CreateVulkanImage(data, width, height, channels); - // Free image data + // Step 2d: Clean up temporary CPU memory to prevent leaks FreeImageData(data); - return Resource::Load(); + return Resource::Load(); // Mark resource as successfully loaded } +---- + +The loading pipeline follows a clear sequence that handles the complex transformation from file-based data to GPU resources. The file path construction assumes a standard naming convention that maps resource IDs to physical files, enabling consistent asset organization across the project. Using the KTX format provides several advantages including GPU-native format storage, mipmap support, and compression compatibility. + +Error handling at each stage prevents partial loading states that could leave the resource in an inconsistent condition. If image data loading fails, the function returns immediately without creating GPU resources, ensuring that the Texture object remains in a clean, unloaded state. This approach prevents resource leaks and makes error recovery more predictable for calling code. + +The temporary nature of the CPU-side image data reflects the typical texture loading workflow where pixel data is needed only long enough to upload to the GPU. Once the GPU resources are created and populated, the CPU copy can be safely discarded, reducing memory pressure and preventing unnecessary data duplication. +=== Texture Resource: GPU Resource Cleanup and Memory Management + +Then, we implement comprehensive resource cleanup that ensures all GPU resources are properly released when the texture is no longer needed, preventing memory leaks in long-running applications. + +[source,cpp] +---- void Unload() override { - // Destroy Vulkan resources + // Only perform cleanup if resource is currently loaded if (IsLoaded()) { - // Get device from somewhere (e.g., singleton or parameter) + // Step 3a: Obtain device handle for resource destruction vk::Device device = GetDevice(); - device.destroySampler(sampler); - device.destroyImageView(imageView); - device.destroyImage(image); - device.freeMemory(memory); + // Step 3b: Destroy GPU objects in reverse creation order + // This ordering prevents use-after-free errors in GPU drivers + device.destroySampler(sampler); // Destroy sampling configuration + device.destroyImageView(imageView); // Destroy shader view + device.destroyImage(image); // Destroy image object + device.freeMemory(memory); // Release GPU memory allocation + // Step 3c: Update base class state to reflect unloaded status Resource::Unload(); } } - // Getters for Vulkan resources + // Public interface for accessing Vulkan resources safely vk::Image GetImage() const { return image; } vk::ImageView GetImageView() const { return imageView; } vk::Sampler GetSampler() const { return sampler; } +---- + +The cleanup sequence follows Vulkan's object dependency requirements, where objects must be destroyed in reverse order of their creation to avoid validation errors and potential driver crashes. The sampler and image view depend on the image, so they must be destroyed first. The memory allocation is released last since it backs the image object. + +The conditional cleanup check prevents double-destruction errors that could occur if Unload() is called multiple times. This safety mechanism is particularly important in resource management systems where multiple code paths might trigger cleanup operations during error handling or shutdown sequences. +The public getter interface provides controlled access to the internal Vulkan resources without exposing the implementation details or allowing external code to modify the resource state. This encapsulation ensures that the Texture object maintains complete control over its GPU resources throughout their lifetime. + +=== Texture Resource: Helper Methods and Implementation Details + +Finally, we provide the supporting infrastructure methods that handle the platform-specific details of image loading and Vulkan resource creation. + +[source,cpp] +---- private: unsigned char* LoadImageData(const std::string& filePath, int* width, int* height, int* channels) { // Implementation using stb_image or ktx library + // This method abstracts the details of different image format support + // and provides a consistent interface for pixel data loading // ... return nullptr; // Placeholder } void FreeImageData(unsigned char* data) { // Implementation using stb_image or ktx library + // Ensures proper cleanup of image loader specific memory allocations + // Different libraries may require different cleanup approaches // ... } void CreateVulkanImage(unsigned char* data, int width, int height, int channels) { // Implementation to create Vulkan image, allocate memory, and upload data + // This involves complex Vulkan operations including: + // - Format selection based on channel count and data type + // - Memory allocation with appropriate usage flags + // - Image creation with optimal tiling and layout + // - Data upload via staging buffers for efficiency + // - Image view creation for shader access + // - Sampler creation with appropriate filtering settings // ... } vk::Device GetDevice() { // Get device from somewhere (e.g., singleton or parameter) + // Production code would use dependency injection or service location + // to provide the Vulkan device handle without tight coupling // ... return vk::Device(); // Placeholder } }; +---- + +The helper methods abstract away the platform-specific and library-specific details of texture loading and GPU resource creation. The `LoadImageData` method encapsulates support for different image formats and loading libraries, providing a consistent interface regardless of whether you're using STB Image, DevIL, FreeImage, or other image loading solutions. +The `CreateVulkanImage` method represents one of the most complex operations in texture management, involving multiple Vulkan API calls with careful attention to format selection, memory alignment, and performance optimization. Production implementations typically use staging buffers for efficient data transfer and may include mipmap generation, format conversion, and compression support. + +The device access pattern shown here as a placeholder represents a common design challenge in resource management systems: how to provide access to core engine services without creating tight coupling. Production systems typically use dependency injection, service locators, or context objects to provide access to the Vulkan device and other core resources. + +=== Mesh Resource Implementation + +The Mesh resource represents the geometric foundation of 3D rendering, managing vertex and index data that define the shape and structure of 3D objects. This implementation demonstrates how to efficiently manage GPU buffer resources for geometric data. + +=== Mesh Resource: Geometric Data Structure and Buffer Management + +First, we establish the fundamental data structures required for storing and managing geometric data on the GPU, including both vertex attributes and index connectivity information. + +[source,cpp] +---- // Mesh resource class Mesh : public Resource { private: - vk::Buffer vertexBuffer; - vk::DeviceMemory vertexBufferMemory; - uint32_t vertexCount = 0; + // Vertex data management - stores per-vertex attributes like position, normal, UV coordinates + vk::Buffer vertexBuffer; // GPU buffer containing vertex attribute data + vk::DeviceMemory vertexBufferMemory; // GPU memory backing the vertex buffer + uint32_t vertexCount = 0; // Number of vertices in this mesh - vk::Buffer indexBuffer; - vk::DeviceMemory indexBufferMemory; - uint32_t indexCount = 0; + // Index data management - defines triangle connectivity using vertex indices + vk::Buffer indexBuffer; // GPU buffer containing triangle index data + vk::DeviceMemory indexBufferMemory; // GPU memory backing the index buffer + uint32_t indexCount = 0; // Number of indices in this mesh (typically 3 per triangle) public: explicit Mesh(const std::string& id) : Resource(id) {} ~Mesh() override { - Unload(); + Unload(); // Ensure GPU resources are cleaned up } +---- + +The mesh resource architecture separates vertex and index data into distinct GPU buffers, following modern graphics API best practices. Vertex buffers contain per-vertex attributes such as positions, normals, texture coordinates, and color information, while index buffers define how vertices connect to form triangles. This separation enables efficient vertex reuse, where a single vertex can be referenced by multiple triangles, significantly reducing memory usage for typical 3D models. + +The buffer-memory pairing reflects Vulkan's explicit memory management model, where buffer objects and their backing memory allocations are managed separately. This approach provides fine-grained control over memory allocation strategies, enabling techniques like memory pooling, suballocation, and custom alignment requirements that can significantly impact rendering performance. + +The count tracking serves dual purposes: it provides essential information for rendering calls that specify how many vertices or indices to process, and it enables validation and debugging by allowing systems to verify that buffer contents match expected data sizes. + +=== Mesh Resource: Data Loading and Format Processing Pipeline +Next, we implement the mesh loading pipeline that transforms file-based geometric data into GPU-ready buffer resources through format parsing and data validation. + +[source,cpp] +---- bool Load() override { - // Load mesh from file + // Step 2a: Construct file path using standardized naming convention std::string filePath = "models/" + GetId() + ".gltf"; - // Load mesh data using a library like tinygltf - std::vector vertices; - std::vector indices; + // Step 2b: Parse geometric data from file format into CPU-accessible structures + std::vector vertices; // Temporary CPU storage for vertex attributes + std::vector indices; // Temporary CPU storage for triangle indices if (!LoadMeshData(filePath, vertices, indices)) { - return false; + return false; // Failed to parse file - abort loading } - // Create Vulkan buffers and upload data - CreateVertexBuffer(vertices); - CreateIndexBuffer(indices); + // Step 2c: Transform CPU data into optimized GPU buffer resources + CreateVertexBuffer(vertices); // Upload vertex attributes to GPU + CreateIndexBuffer(indices); // Upload triangle connectivity to GPU + // Step 2d: Cache metadata for efficient rendering operations vertexCount = static_cast(vertices.size()); indexCount = static_cast(indices.size()); - return Resource::Load(); + return Resource::Load(); // Mark resource as successfully loaded } +---- + +The loading pipeline follows a structured approach that separates file parsing from GPU resource creation, enabling better error handling and code reusability. The choice of glTF format provides several advantages including industry-standard mesh representation, embedded material information, and support for advanced features like skeletal animations and morph targets. +The temporary CPU-side storage approach enables validation and processing of geometric data before committing to GPU resources. This intermediate step allows for mesh optimization techniques such as vertex cache optimization, triangle strip generation, or level-of-detail processing that can significantly improve rendering performance. + +The metadata caching strategy stores frequently accessed information locally to avoid expensive GPU queries during rendering. These counts are essential for draw calls, where the GPU needs to know exactly how many vertices to process and how many triangles to render, making local storage much more efficient than querying the GPU buffers repeatedly. + +=== Mesh Resource — Then: GPU Resource Cleanup and Memory Reclamation + +Then, we implement comprehensive cleanup that properly releases all GPU resources and memory allocations when the mesh is no longer needed, ensuring robust memory management in long-running applications. + +[source,cpp] +---- void Unload() override { - // Destroy Vulkan resources + // Only proceed with cleanup if resources are currently loaded if (IsLoaded()) { - // Get device from somewhere (e.g., singleton or parameter) + // Phase 3a: Obtain device handle for resource destruction vk::Device device = GetDevice(); - device.destroyBuffer(indexBuffer); - device.freeMemory(indexBufferMemory); + // Phase 3b: Destroy buffers and free GPU memory in proper sequence + // Index resources cleaned up first to maintain clear dependency order + device.destroyBuffer(indexBuffer); // Destroy index buffer object + device.freeMemory(indexBufferMemory); // Release index buffer memory - device.destroyBuffer(vertexBuffer); - device.freeMemory(vertexBufferMemory); + // Vertex resources cleaned up second + device.destroyBuffer(vertexBuffer); // Destroy vertex buffer object + device.freeMemory(vertexBufferMemory); // Release vertex buffer memory + // Phase 3c: Update base class state to reflect unloaded condition Resource::Unload(); } } - // Getters for Vulkan resources + // Public interface for safe access to GPU resources and metadata vk::Buffer GetVertexBuffer() const { return vertexBuffer; } vk::Buffer GetIndexBuffer() const { return indexBuffer; } uint32_t GetVertexCount() const { return vertexCount; } uint32_t GetIndexCount() const { return indexCount; } +---- + +The cleanup sequence ensures that GPU resources are properly released without causing validation errors or driver instability. While Vulkan doesn't impose strict ordering requirements for buffer destruction, following a consistent pattern (index resources before vertex resources) makes the code more predictable and easier to debug when issues arise. + +The conditional cleanup check prevents double-destruction scenarios that could occur during error handling or when multiple systems attempt to clean up resources simultaneously. This safety mechanism is particularly important in complex rendering systems where resource ownership might be shared between multiple components. + +The public access interface provides controlled access to internal GPU resources while maintaining encapsulation. These getter methods enable rendering systems to bind the appropriate buffers for draw operations while preventing external code from accidentally modifying the mesh's internal state or triggering premature resource destruction. + +=== Mesh Resource: Helper Methods and Implementation Support Infrastructure +The final phase provides the supporting methods that handle the complex details of mesh data parsing, buffer creation, and system integration required for complete mesh resource functionality. + +[source,cpp] +---- private: bool LoadMeshData(const std::string& filePath, std::vector& vertices, std::vector& indices) { // Implementation using tinygltf or similar library + // This method handles the complex task of: + // - Opening and validating the mesh file format + // - Parsing vertex attributes (positions, normals, UVs, etc.) + // - Extracting index data that defines triangle connectivity + // - Converting from file format to engine-specific vertex structures + // - Performing validation to ensure data integrity // ... return true; // Placeholder } void CreateVertexBuffer(const std::vector& vertices) { // Implementation to create Vulkan buffer, allocate memory, and upload data + // This involves several complex Vulkan operations: + // - Calculating buffer size requirements based on vertex count and structure + // - Creating buffer with appropriate usage flags (vertex buffer usage) + // - Allocating GPU memory with optimal memory type selection + // - Uploading data via staging buffer for efficient transfer + // - Setting up memory barriers to ensure data availability // ... } void CreateIndexBuffer(const std::vector& indices) { // Implementation to create Vulkan buffer, allocate memory, and upload data + // Similar to vertex buffer creation but optimized for index data: + // - Buffer creation with index buffer specific usage flags + // - Memory allocation optimized for read-heavy access patterns + // - Efficient data transfer using appropriate staging mechanisms + // - Index format validation (16-bit vs 32-bit indices) // ... } vk::Device GetDevice() { // Get device from somewhere (e.g., singleton or parameter) + // Production implementations typically use dependency injection + // to avoid tight coupling between resource classes and core engine systems // ... return vk::Device(); // Placeholder } }; +---- + +The helper methods encapsulate the most complex aspects of mesh resource management, hiding implementation details while providing clean interfaces for the core loading and creation logic. The `LoadMeshData` method abstracts the intricacies of different mesh file formats and parsing libraries, enabling the resource system to support multiple formats through a consistent interface. + +The buffer creation methods represent some of the most performance-critical code in the mesh resource system, as inefficient GPU memory management can significantly impact rendering performance. Production implementations typically use staging buffers for data upload, implement memory pooling to reduce allocation overhead, and carefully select memory types based on GPU architecture characteristics. + +The device access pattern illustrates a common architectural challenge in resource management systems: balancing convenience with loose coupling. While direct access to global singletons can simplify implementation, production systems typically use dependency injection or service locator patterns to maintain testability and flexibility while providing access to core engine services. // Shader resource class Shader : public Resource { diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc index a41184d7..225752c9 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Rendering Pipeline -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Rendering Pipeline @@ -51,15 +44,7 @@ When designing a rendering pipeline, you'll need to address several challenges: === Rendering Pipeline Architecture -A modern rendering pipeline typically consists of several stages: - -1. *Scene Culling* - Determine which objects are visible and need to be rendered. -2. *Render Pass Management* - Organize rendering into passes with specific purposes. -3. *Command Generation* - Generate commands for the GPU to execute. -4. *Execution* - Submit commands to the GPU for execution. -5. *Post-Processing* - Apply effects to the rendered image. - -Let's explore each of these stages in detail. +Earlier we outlined the major stages of a modern pipeline. Rather than repeating that list, we'll now dive into each stage, focusing on responsibilities, data flow, and practical implementation patterns that keep the engine flexible and performant. === Scene Culling @@ -136,305 +121,388 @@ A rendergraph is a data structure that: 4. *Handles Synchronization*: Automatically inserts necessary synchronization primitives. 5. *Optimizes Memory*: Can perform memory aliasing and other optimizations. +=== Rendergraph: Data Structure Architecture and Resource Representation + +First, we need to establish the fundamental data structures that represent rendering resources and passes within the rendergraph system. + [source,cpp] ---- -// A simple rendergraph class +// A comprehensive rendergraph implementation for automated dependency management class Rendergraph { private: + // Resource description and management structure + // Represents any GPU resource used during rendering (textures, render targets, buffers) struct Resource { - std::string name; - vk::Format format; - vk::Extent2D extent; - vk::ImageUsageFlags usage; - vk::ImageLayout initialLayout; - vk::ImageLayout finalLayout; - - // The actual Vulkan resources - vk::raii::Image image = nullptr; - vk::raii::DeviceMemory memory = nullptr; - vk::raii::ImageView view = nullptr; + std::string name; // Human-readable identifier for debugging and referencing + vk::Format format; // Pixel format (RGBA8, Depth24Stencil8, etc.) + vk::Extent2D extent; // Dimensions in pixels for 2D resources + vk::ImageUsageFlags usage; // How this resource will be used (color attachment, texture, etc.) + vk::ImageLayout initialLayout; // Expected layout when the frame begins + vk::ImageLayout finalLayout; // Required layout when the frame ends + + // Actual GPU resources - populated during compilation + vk::raii::Image image = nullptr; // The GPU image object + vk::raii::DeviceMemory memory = nullptr; // Backing memory allocation + vk::raii::ImageView view = nullptr; // Shader-accessible view of the image }; + // Render pass representation within the graph structure + // Each pass represents a distinct rendering operation with defined inputs and outputs struct Pass { - std::string name; - std::vector inputs; // Resources read by this pass - std::vector outputs; // Resources written by this pass - std::function executeFunc; + std::string name; // Descriptive name for debugging and profiling + std::vector inputs; // Resources this pass reads from (dependencies) + std::vector outputs; // Resources this pass writes to (products) + std::function executeFunc; // The actual rendering code }; - std::unordered_map resources; - std::vector passes; - std::vector executionOrder; // Indices into passes + // Core data storage for the rendergraph system + std::unordered_map resources; // All resources referenced in the graph + std::vector passes; // All rendering passes in definition order + std::vector executionOrder; // Computed optimal execution sequence - // Synchronization objects - std::vector semaphores; - std::vector> semaphoreSignalWaitPairs; // (signaling pass, waiting pass) + // Automatic synchronization management + // These objects ensure correct GPU execution order without manual barriers + std::vector semaphores; // GPU synchronization primitives + std::vector> semaphoreSignalWaitPairs; // (signaling pass, waiting pass) - vk::raii::Device& device; + vk::raii::Device& device; // Vulkan device for resource creation public: explicit Rendergraph(vk::raii::Device& dev) : device(dev) {} +---- + +The data structure architecture reflects the core philosophy of rendergraphs: treating rendering as a series of transformations on resources rather than imperative GPU commands. The Resource structure encapsulates everything needed to create and manage GPU resources, while the Pass structure defines rendering operations in terms of their resource dependencies rather than their implementation details. + +This approach enables powerful optimizations like automatic memory aliasing (where multiple resources share the same memory if their lifetimes don't overlap) and optimal resource layout transitions. The separation between resource description and actual GPU objects allows the rendergraph to make informed decisions about resource management during the compilation phase. - // Add a resource to the rendergraph +=== Rendergraph: Resource Registration and Pass Definition Interface + +Now for the public interface for building the rendergraph by registering resources and defining rendering passes with their dependencies. + +[source,cpp] +---- + // Resource registration interface for declaring all resources used during rendering + // This method establishes resource metadata without creating actual GPU resources void AddResource(const std::string& name, vk::Format format, vk::Extent2D extent, vk::ImageUsageFlags usage, vk::ImageLayout initialLayout, vk::ImageLayout finalLayout) { Resource resource; - resource.name = name; - resource.format = format; - resource.extent = extent; - resource.usage = usage; - resource.initialLayout = initialLayout; - resource.finalLayout = finalLayout; - - resources[name] = resource; + resource.name = name; // Store human-readable identifier + resource.format = format; // Define pixel format and bit depth + resource.extent = extent; // Set resource dimensions + resource.usage = usage; // Specify intended usage patterns + resource.initialLayout = initialLayout; // Define starting layout state + resource.finalLayout = finalLayout; // Define required ending state + + resources[name] = resource; // Register in the global resource map } - // Add a pass to the rendergraph + // Pass registration interface for defining rendering operations and their dependencies + // This method establishes the logical structure of rendering without immediate execution void AddPass(const std::string& name, const std::vector& inputs, const std::vector& outputs, std::function executeFunc) { Pass pass; - pass.name = name; - pass.inputs = inputs; - pass.outputs = outputs; - pass.executeFunc = executeFunc; + pass.name = name; // Assign descriptive identifier + pass.inputs = inputs; // List all resources this pass reads + pass.outputs = outputs; // List all resources this pass writes + pass.executeFunc = executeFunc; // Store the actual rendering implementation - passes.push_back(pass); + passes.push_back(pass); // Add to the ordered pass list } +---- + +The registration interface enables declarative rendergraph construction where developers specify what they want to achieve rather than how to achieve it. This high-level approach allows the rendergraph to analyze the entire rendering pipeline before making resource allocation and scheduling decisions. + +The deferred execution model (where passes store function objects rather than immediate GPU commands) enables powerful compile-time optimizations. The rendergraph can reorder passes, merge compatible operations, and optimize resource usage based on the complete dependency graph rather than making local decisions for each pass. + +=== Rendergraph: Dependency Analysis and Execution Ordering - // Compile the rendergraph +Now we implement the core algorithmic logic that analyzes pass dependencies and computes an optimal execution order for the rendering pipeline. + +[source,cpp] +---- + // Rendergraph compilation - transforms declarative descriptions into executable pipeline + // This method performs dependency analysis, resource allocation, and execution planning void Compile() { - // Build the dependency graph - std::vector> dependencies(passes.size()); - std::vector> dependents(passes.size()); + // Dependency Graph Construction + // Build bidirectional dependency relationships between passes + std::vector> dependencies(passes.size()); // What each pass depends on + std::vector> dependents(passes.size()); // What depends on each pass - // Map resources to the passes that write to them + // Track which pass produces each resource (write-after-write dependencies) std::unordered_map resourceWriters; - // Find dependencies based on resource usage + // Dependency Discovery Through Resource Usage Analysis + // Analyze each pass to determine data flow relationships for (size_t i = 0; i < passes.size(); ++i) { const auto& pass = passes[i]; - // Check inputs + // Process input dependencies - this pass must wait for producers for (const auto& input : pass.inputs) { auto it = resourceWriters.find(input); if (it != resourceWriters.end()) { - // This pass depends on the pass that writes to this resource - dependencies[i].push_back(it->second); - dependents[it->second].push_back(i); + // Found the pass that produces this input - create dependency link + dependencies[i].push_back(it->second); // This pass depends on the producer + dependents[it->second].push_back(i); // Producer has this as dependent } } - // Record outputs + // Register output production - subsequent passes may depend on these for (const auto& output : pass.outputs) { - resourceWriters[output] = i; + resourceWriters[output] = i; // Record this pass as producer } } - // Topological sort to determine execution order - std::vector visited(passes.size(), false); - std::vector inStack(passes.size(), false); + // Topological Sort for Optimal Execution Order + // Use depth-first search to compute valid execution sequence while detecting cycles + std::vector visited(passes.size(), false); // Track completed nodes + std::vector inStack(passes.size(), false); // Track current recursion path + std::function visit = [&](size_t node) { if (inStack[node]) { + // Cycle detection - circular dependency found throw std::runtime_error("Cycle detected in rendergraph"); } if (visited[node]) { - return; + return; // Already processed this node and its dependencies } - inStack[node] = true; + inStack[node] = true; // Mark as currently being processed + // Recursively process all dependent passes first (post-order traversal) for (auto dependent : dependents[node]) { visit(dependent); } - inStack[node] = false; - visited[node] = true; - executionOrder.push_back(node); + inStack[node] = false; // Remove from current path + visited[node] = true; // Mark as completely processed + executionOrder.push_back(node); // Add to execution sequence }; + // Process all unvisited nodes to handle disconnected graph components for (size_t i = 0; i < passes.size(); ++i) { if (!visited[i]) { visit(i); } } +---- - // Create synchronization objects +The dependency analysis represents the mathematical core of the rendergraph system, transforming an abstract description of rendering operations into a concrete execution plan. The bidirectional dependency tracking enables efficient graph traversal algorithms and provides the information needed for automatic synchronization. + +The topological sort algorithm ensures that passes execute in dependency order while detecting impossible circular dependencies that would represent logical errors in the rendering pipeline design. This compile-time validation catches many common rendering pipeline bugs before they manifest as runtime GPU synchronization issues. + +=== Rendergraph: Automatic Synchronization and Resource Allocation + +Next create the GPU synchronization objects needed for correct execution ordering and allocates the actual Vulkan resources for all registered resources. + +[source,cpp] +---- + // Automatic Synchronization Object Creation + // Generate semaphores for all dependencies identified during analysis for (size_t i = 0; i < passes.size(); ++i) { for (auto dep : dependencies[i]) { - // Create a semaphore for this dependency + // Create a GPU semaphore for this dependency relationship + // The dependent pass will wait on this semaphore before executing semaphores.emplace_back(device.createSemaphore({})); - semaphoreSignalWaitPairs.emplace_back(dep, i); + semaphoreSignalWaitPairs.emplace_back(dep, i); // (producer, consumer) pair } } - // Allocate actual resources + // Physical Resource Allocation and Creation + // Transform resource descriptions into actual GPU objects for (auto& [name, resource] : resources) { - // Create image + // Configure image creation parameters based on resource description vk::ImageCreateInfo imageInfo; - imageInfo.setImageType(vk::ImageType::e2D) - .setFormat(resource.format) - .setExtent({resource.extent.width, resource.extent.height, 1}) - .setMipLevels(1) - .setArrayLayers(1) - .setSamples(vk::SampleCountFlagBits::e1) - .setTiling(vk::ImageTiling::eOptimal) - .setUsage(resource.usage) - .setSharingMode(vk::SharingMode::eExclusive) - .setInitialLayout(vk::ImageLayout::eUndefined); - - resource.image = device.createImage(imageInfo); - - // Allocate memory + imageInfo.setImageType(vk::ImageType::e2D) // 2D texture/render target + .setFormat(resource.format) // Pixel format from description + .setExtent({resource.extent.width, resource.extent.height, 1}) // Dimensions + .setMipLevels(1) // Single mip level for simplicity + .setArrayLayers(1) // Single layer (not array texture) + .setSamples(vk::SampleCountFlagBits::e1) // No multisampling + .setTiling(vk::ImageTiling::eOptimal) // GPU-optimal memory layout + .setUsage(resource.usage) // Usage flags from registration + .setSharingMode(vk::SharingMode::eExclusive) // Single queue family access + .setInitialLayout(vk::ImageLayout::eUndefined); // Initial layout (will be transitioned) + + resource.image = device.createImage(imageInfo); // Create the GPU image object + + // Allocate backing memory for the image vk::MemoryRequirements memRequirements = resource.image.getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo; - allocInfo.setAllocationSize(memRequirements.size) + allocInfo.setAllocationSize(memRequirements.size) // Required memory size .setMemoryTypeIndex(FindMemoryType(memRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eDeviceLocal)); + vk::MemoryPropertyFlagBits::eDeviceLocal)); // GPU-local memory - resource.memory = device.allocateMemory(allocInfo); - resource.image.bindMemory(*resource.memory, 0); + resource.memory = device.allocateMemory(allocInfo); // Allocate GPU memory + resource.image.bindMemory(*resource.memory, 0); // Bind memory to image - // Create image view + // Create image view for shader access vk::ImageViewCreateInfo viewInfo; - viewInfo.setImage(*resource.image) - .setViewType(vk::ImageViewType::e2D) - .setFormat(resource.format) - .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + viewInfo.setImage(*resource.image) // Reference the created image + .setViewType(vk::ImageViewType::e2D) // 2D view type + .setFormat(resource.format) // Match image format + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); // Full image access - resource.view = device.createImageView(viewInfo); + resource.view = device.createImageView(viewInfo); // Create shader-accessible view } } - // Execute the rendergraph - void Execute(vk::raii::CommandBuffer& commandBuffer, vk::Queue queue) { - std::vector cmdBuffers; - std::vector waitSemaphores; - std::vector waitStages; - std::vector signalSemaphores; + // Resource access interface for retrieving compiled resources + Resource* GetResource(const std::string& name) { + auto it = resources.find(name); + return (it != resources.end()) ? &it->second : nullptr; + } +---- + +=== Rendergraph: Execution Engine and Command Recording + +Finally, implement the execution engine that coordinates pass execution with proper synchronization and resource transitions. + +[source,cpp] +---- - // For each pass in the execution order + // Rendergraph execution engine - coordinates pass execution with automatic synchronization + // This method transforms the compiled rendergraph into actual GPU work + void Execute(vk::raii::CommandBuffer& commandBuffer, vk::Queue queue) { + // Execution state management for dynamic synchronization + std::vector cmdBuffers; // Command buffer storage + std::vector waitSemaphores; // Synchronization dependencies for current pass + std::vector waitStages; // Pipeline stages to wait on + std::vector signalSemaphores; // Semaphores to signal after current pass + + // Ordered Pass Execution with Automatic Dependency Management + // Execute each pass in the computed dependency-safe order for (auto passIdx : executionOrder) { const auto& pass = passes[passIdx]; - // Collect wait semaphores for this pass + // Synchronization Setup - Collect Dependencies for Current Pass + // Determine what this pass must wait for before executing waitSemaphores.clear(); waitStages.clear(); for (size_t i = 0; i < semaphoreSignalWaitPairs.size(); ++i) { if (semaphoreSignalWaitPairs[i].second == passIdx) { - waitSemaphores.push_back(*semaphores[i]); - waitStages.push_back(vk::PipelineStageFlagBits::eColorAttachmentOutput); + // This pass depends on the completion of another pass + waitSemaphores.push_back(*semaphores[i]); // Wait for dependency completion + waitStages.push_back(vk::PipelineStageFlagBits::eColorAttachmentOutput); // Wait at output stage } } - // Collect signal semaphores for this pass + // Collect semaphores that this pass will signal for dependent passes signalSemaphores.clear(); - for (size_t i = 0; i < semaphoreSignalWaitPairs.size(); ++i) { if (semaphoreSignalWaitPairs[i].first == passIdx) { - signalSemaphores.push_back(*semaphores[i]); + // Other passes depend on this pass's completion + signalSemaphores.push_back(*semaphores[i]); // Signal completion for dependents } } - // Begin command buffer - commandBuffer.begin({}); + // Command Buffer Preparation and Resource Layout Transitions + // Set up command recording and transition resources to appropriate layouts + commandBuffer.begin({}); // Begin command recording - // Insert image memory barriers for layout transitions + // Transition input resources to shader-readable layouts for (const auto& input : pass.inputs) { auto& resource = resources[input]; vk::ImageMemoryBarrier barrier; - barrier.setOldLayout(resource.initialLayout) - .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) - .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + barrier.setOldLayout(resource.initialLayout) // Current resource layout + .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) // Target layout for reading + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) // No queue family transfer .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) - .setImage(*resource.image) - .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) - .setSrcAccessMask(vk::AccessFlagBits::eMemoryWrite) - .setDstAccessMask(vk::AccessFlagBits::eShaderRead); + .setImage(*resource.image) // Target image + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) // Full image range + .setSrcAccessMask(vk::AccessFlagBits::eMemoryWrite) // Previous write access + .setDstAccessMask(vk::AccessFlagBits::eShaderRead); // Required read access + // Insert pipeline barrier for safe layout transition commandBuffer.pipelineBarrier( - vk::PipelineStageFlagBits::eAllCommands, - vk::PipelineStageFlagBits::eFragmentShader, - vk::DependencyFlagBits::eByRegion, - 0, nullptr, - 0, nullptr, - 1, &barrier + vk::PipelineStageFlagBits::eAllCommands, // Wait for all previous work + vk::PipelineStageFlagBits::eFragmentShader, // Enable fragment shader access + vk::DependencyFlagBits::eByRegion, // Region-local dependency + 0, nullptr, 0, nullptr, 1, &barrier // Image barrier only ); } + // Transition output resources to render target layouts for (const auto& output : pass.outputs) { auto& resource = resources[output]; vk::ImageMemoryBarrier barrier; - barrier.setOldLayout(resource.initialLayout) - .setNewLayout(vk::ImageLayout::eColorAttachmentOptimal) + barrier.setOldLayout(resource.initialLayout) // Current layout state + .setNewLayout(vk::ImageLayout::eColorAttachmentOptimal) // Optimal for color output .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .setImage(*resource.image) .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) - .setSrcAccessMask(vk::AccessFlagBits::eMemoryRead) - .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); + .setSrcAccessMask(vk::AccessFlagBits::eMemoryRead) // Previous read access + .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); // Required write access + // Insert barrier for safe transition to writable state commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eAllCommands, - vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eColorAttachmentOutput, // Enable color attachment writes vk::DependencyFlagBits::eByRegion, - 0, nullptr, - 0, nullptr, - 1, &barrier + 0, nullptr, 0, nullptr, 1, &barrier ); } - // Execute the pass - pass.executeFunc(commandBuffer); + // Pass Execution - Execute the Actual Rendering Logic + // Call the user-provided rendering function with prepared command buffer + pass.executeFunc(commandBuffer); // Execute pass-specific rendering - // Insert image memory barriers for final layout transitions + // Final Layout Transitions - Prepare Resources for Subsequent Use + // Transition output resources to their final required layouts for (const auto& output : pass.outputs) { auto& resource = resources[output]; vk::ImageMemoryBarrier barrier; - barrier.setOldLayout(vk::ImageLayout::eColorAttachmentOptimal) - .setNewLayout(resource.finalLayout) + barrier.setOldLayout(vk::ImageLayout::eColorAttachmentOptimal) // Current writable layout + .setNewLayout(resource.finalLayout) // Required final layout .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .setImage(*resource.image) .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) - .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite) - .setDstAccessMask(vk::AccessFlagBits::eMemoryRead); + .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite) // Previous write operations + .setDstAccessMask(vk::AccessFlagBits::eMemoryRead); // Enable subsequent reads + // Insert final barrier for layout transition commandBuffer.pipelineBarrier( - vk::PipelineStageFlagBits::eColorAttachmentOutput, - vk::PipelineStageFlagBits::eAllCommands, + vk::PipelineStageFlagBits::eColorAttachmentOutput, // After color writes complete + vk::PipelineStageFlagBits::eAllCommands, // Before any subsequent work vk::DependencyFlagBits::eByRegion, - 0, nullptr, - 0, nullptr, - 1, &barrier + 0, nullptr, 0, nullptr, 1, &barrier ); } - // End command buffer - commandBuffer.end(); + // Command Submission with Synchronization + // Submit command buffer with appropriate dependency and signaling semaphores + commandBuffer.end(); // Finalize command recording - // Submit command buffer vk::SubmitInfo submitInfo; - submitInfo.setWaitSemaphoreCount(static_cast(waitSemaphores.size())) - .setPWaitSemaphores(waitSemaphores.data()) - .setPWaitDstStageMask(waitStages.data()) - .setCommandBufferCount(1) - .setPCommandBuffers(&*commandBuffer) - .setSignalSemaphoreCount(static_cast(signalSemaphores.size())) - .setPSignalSemaphores(signalSemaphores.data()); - - queue.submit(1, &submitInfo, nullptr); + submitInfo.setWaitSemaphoreCount(static_cast(waitSemaphores.size())) // Dependencies to wait for + .setPWaitSemaphores(waitSemaphores.data()) // Dependency semaphores + .setPWaitDstStageMask(waitStages.data()) // Pipeline stages to wait at + .setCommandBufferCount(1) // Single command buffer + .setPCommandBuffers(&*commandBuffer) // Command buffer to execute + .setSignalSemaphoreCount(static_cast(signalSemaphores.size())) // Semaphores to signal + .setPSignalSemaphores(signalSemaphores.data()); // Signal semaphores + + queue.submit(1, &submitInfo, nullptr); // Submit to GPU queue } } +---- + +The execution engine represents the culmination of the rendergraph system, where all the analysis and preparation work pays off in coordinated GPU execution. The automatic synchronization ensures that passes execute in the correct order without manual barrier management, while the automatic layout transitions handle the complex state management that Vulkan requires for optimal performance. +This execution model demonstrates the power of the rendergraph abstraction: complex multi-pass rendering with dozens of resources and dependencies gets reduced to a simple `Execute()` call, with all the synchronization and resource management handled automatically based on the declarative pass and resource descriptions. + +[source,cpp] +---- private: uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { // Implementation to find suitable memory type @@ -446,7 +514,7 @@ private: ==== Vulkan Synchronization -Vulkan provides several synchronization primitives to ensure correct execution order and memory visibility: +Synchronization in Vulkan is one of the most complicated topics. Vulkan provides several synchronization primitives to ensure correct execution order and memory visibility: 1. *Semaphores*: Used for synchronization between queue operations (GPU-GPU synchronization). 2. *Fences*: Used for synchronization between CPU and GPU. @@ -469,100 +537,211 @@ The vulkan tutorial includes a more detailed discussion of synchronization, the ===== Pipeline Barriers -Pipeline barriers are one of the most important synchronization primitives in Vulkan. They ensure that operations before the barrier complete before operations after the barrier begin, and they can also perform layout transitions for images. +Pipeline barriers are one of the most important synchronization primitives in Vulkan. They ensure that operations before the barrier are complete before operations after the barrier begin, and they can also perform layout transitions for images. Let's examine how to implement proper image layout transitions through a comprehensive breakdown of the process. + +=== Image Layout Transition: Barrier Configuration and Resource Specification + +First, we establish the basic barrier structure and identify which image resource needs to transition between layouts. [source,cpp] ---- -// Example of using a pipeline barrier for an image layout transition +// Comprehensive image layout transition implementation +// This function demonstrates proper synchronization and layout management in Vulkan void TransitionImageLayout(vk::raii::CommandBuffer& commandBuffer, vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { + + // Configure the basic image memory barrier structure + // This barrier will coordinate memory access and layout transitions vk::ImageMemoryBarrier barrier; - barrier.setOldLayout(oldLayout) - .setNewLayout(newLayout) - .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) - .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) - .setImage(image) - .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + barrier.setOldLayout(oldLayout) // Current image layout state + .setNewLayout(newLayout) // Target layout after transition + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) // No queue family ownership transfer + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) // Same queue family throughout + .setImage(image) // Target image for the transition + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); // Full color image range +---- + +The image memory barrier serves as the fundamental mechanism for coordinating both memory access patterns and image layout transitions in Vulkan. Unlike OpenGL where these operations happen automatically, Vulkan requires explicit specification of when and how image layouts change. The queue family settings using VK_QUEUE_FAMILY_IGNORED indicate that we're not transferring ownership between different queue families, which simplifies the synchronization requirements. + +The subresource range specification defines exactly which portions of the image are affected by this barrier. In this case, we're transitioning the entire color aspect of the image across all mip levels and array layers, which is the most common scenario for basic texture operations. + +=== Image Layout Transition: Pipeline Stage and Access Mask Determination - vk::PipelineStageFlags sourceStage; - vk::PipelineStageFlags destinationStage; +Next, we analyze the specific layout transition being performed and determine the appropriate pipeline stages and memory access patterns for optimal synchronization. +[source,cpp] +---- + // Initialize pipeline stage tracking for synchronization timing + // These stages define when operations must complete and when new operations can begin + vk::PipelineStageFlags sourceStage; // When previous operations must finish + vk::PipelineStageFlags destinationStage; // When subsequent operations can start + + // Configure synchronization for undefined-to-transfer layout transitions + // This pattern is common when preparing images for data uploads if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { - barrier.setSrcAccessMask(vk::AccessFlagBits::eNone) - .setDstAccessMask(vk::AccessFlagBits::eTransferWrite); - sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; - destinationStage = vk::PipelineStageFlagBits::eTransfer; + // Configure memory access permissions for upload preparation + barrier.setSrcAccessMask(vk::AccessFlagBits::eNone) // No previous access to synchronize + .setDstAccessMask(vk::AccessFlagBits::eTransferWrite); // Enable transfer write operations + + // Set pipeline stage synchronization points for upload workflow + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; // No previous work to wait for + destinationStage = vk::PipelineStageFlagBits::eTransfer; // Transfer operations can proceed + + // Configure synchronization for transfer-to-shader layout transitions + // This pattern prepares uploaded images for shader sampling } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier.setSrcAccessMask(vk::AccessFlagBits::eTransferWrite) - .setDstAccessMask(vk::AccessFlagBits::eShaderRead); - sourceStage = vk::PipelineStageFlagBits::eTransfer; - destinationStage = vk::PipelineStageFlagBits::eFragmentShader; + // Configure memory access transition from writing to reading + barrier.setSrcAccessMask(vk::AccessFlagBits::eTransferWrite) // Previous transfer writes must complete + .setDstAccessMask(vk::AccessFlagBits::eShaderRead); // Enable shader read access + + // Set pipeline stage synchronization for shader usage workflow + sourceStage = vk::PipelineStageFlagBits::eTransfer; // Transfer operations must complete + destinationStage = vk::PipelineStageFlagBits::eFragmentShader; // Fragment shaders can access + } else { + // Handle unsupported transition combinations + // Production code would include additional common transition patterns throw std::invalid_argument("Unsupported layout transition!"); } +---- + +The pipeline stage and access mask configuration represents the heart of Vulkan's explicit synchronization model. By specifying exactly which operations must complete before the barrier (source stage) and which operations can begin after the barrier (destination stage), we create precise control over GPU execution timing without unnecessary stalls. + +The access mask patterns define the memory visibility requirements for each transition. The transition from "no access" to "transfer write" enables efficient image upload without waiting for non-existent previous operations. The transition from "transfer write" to "shader read" ensures that uploaded data is fully written and visible before shaders attempt to sample from the texture. + +=== Image Layout Transition: Barrier Execution and GPU Synchronization + +Finally, we submit the configured barrier to the GPU command stream, ensuring that the layout transition and synchronization occur at the correct point in the rendering pipeline. +[source,cpp] +---- + // Execute the pipeline barrier with configured synchronization + // This commits the layout transition and memory synchronization to the command buffer commandBuffer.pipelineBarrier( - sourceStage, destinationStage, - vk::DependencyFlagBits::eByRegion, - 0, nullptr, - 0, nullptr, - 1, &barrier + sourceStage, // Wait for these operations to complete + destinationStage, // Before allowing these operations to begin + vk::DependencyFlagBits::eByRegion, // Enable region-local optimizations + 0, nullptr, // No memory barriers needed + 0, nullptr, // No buffer barriers needed + 1, &barrier // Apply our image memory barrier ); } ---- -===== Semaphores and Fences +The pipeline barrier submission represents the culmination of our synchronization planning, where the configured barrier becomes part of the GPU's command stream. The `ByRegion` dependency flag enables GPU optimizations for cases where different regions of the image can be processed independently, potentially improving performance on tile-based renderers and other advanced GPU architectures. + +The parameter structure clearly separates different types of barriers (memory, buffer, and image), allowing the GPU driver to apply the most efficient synchronization strategy for each resource type. In our case, we only need image barrier synchronization, so the other barrier arrays remain empty, avoiding unnecessary overhead. + +==== Semaphores and Fences -Semaphores and fences are used for coarser-grained synchronization: +Semaphores and fences are used for coarser-grained synchronization between different stages of the rendering pipeline and between CPU and GPU operations. Let's examine how to properly coordinate frame rendering using these synchronization primitives through a comprehensive breakdown of the frame rendering process. + +=== Frame Rendering: CPU-GPU Synchronization and Frame Pacing + +First, we ensure proper coordination between CPU frame preparation and GPU execution, preventing the CPU from getting too far ahead of the GPU and managing resource contention. [source,cpp] ---- -// Example of using semaphores and fences for queue synchronization +// Comprehensive frame rendering with proper synchronization +// This function demonstrates the complete cycle of frame rendering coordination void RenderFrame(vk::raii::Device& device, vk::Queue graphicsQueue, vk::Queue presentQueue) { - // Wait for the previous frame to finish + + // Synchronize with previous frame completion + // Prevent CPU from submitting work faster than GPU can process it vk::Result result = device.waitForFences(1, &*inFlightFence, VK_TRUE, UINT64_MAX); + + // Reset fence for this frame's completion tracking + // Prepare the fence to signal when this frame's GPU work completes device.resetFences(1, &*inFlightFence); +---- + +The fence-based synchronization serves as the primary mechanism for CPU-GPU coordination in frame rendering. By waiting for the previous frame's fence, we ensure that the GPU has completed processing the previous frame before beginning work on the current frame. This prevents the CPU from overwhelming the GPU with work and helps maintain stable frame pacing. + +The fence reset operation prepares the synchronization object for the current frame. Fences are binary signals that transition from unsignaled to signaled state when associated GPU work completes, so they must be explicitly reset before reuse. The timeout value UINT64_MAX effectively means "wait indefinitely," which is appropriate for frame synchronization where we must ensure completion. + +=== Frame Rendering: Swapchain Image Acquisition and Resource Preparation - // Acquire the next image from the swapchain +Next, we acquire the next available swapchain image for rendering, coordinating with the presentation engine to ensure proper image availability. + +[source,cpp] +---- + // Acquire next available image from the swapchain + // This operation coordinates with the presentation engine and display system uint32_t imageIndex; - result = device.acquireNextImageKHR(*swapchain, UINT64_MAX, - *imageAvailableSemaphore, nullptr, &imageIndex); + result = device.acquireNextImageKHR(*swapchain, // Target swapchain for acquisition + UINT64_MAX, // Wait indefinitely for image availability + *imageAvailableSemaphore, // Semaphore signaled when image is available + nullptr, // No fence needed for this operation + &imageIndex); // Receives index of acquired image + + // Record command buffer for this frame's rendering + // Command buffer recording happens here with acquired image as render target + // ... (command recording implementation would go here) +---- - // Record command buffer - // ... +The swapchain image acquisition represents a critical synchronization point between the rendering system and the presentation engine. The operation may block if no images are currently available (for example, if all swapchain images are being displayed or processed), making it essential for frame pacing. The semaphore signaled by this operation will be used later to ensure that rendering doesn't begin until the acquired image is truly available for modification. - // Submit command buffer +The indefinite timeout ensures that acquisition will eventually succeed even under heavy load or when dealing with variable refresh rate displays. The acquired image index determines which swapchain image becomes the render target for this frame, affecting descriptor set bindings and render pass configuration in the subsequent command recording phase. + +=== Frame Rendering: GPU Work Submission and Inter-Queue Synchronization + +Next, we submit the recorded rendering commands to the GPU with proper synchronization to coordinate between image acquisition, rendering, and presentation operations. + +[source,cpp] +---- + // Configure GPU work submission with comprehensive synchronization + // This submission coordinates image availability, rendering, and presentation readiness vk::SubmitInfo submitInfo; vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; - submitInfo.setWaitSemaphoreCount(1) - .setPWaitSemaphores(&*imageAvailableSemaphore) - .setPWaitDstStageMask(waitStages) - .setCommandBufferCount(1) - .setPCommandBuffers(&*commandBuffer) - .setSignalSemaphoreCount(1) - .setPSignalSemaphores(&*renderFinishedSemaphore); + submitInfo.setWaitSemaphoreCount(1) // Wait for one semaphore before execution + .setPWaitSemaphores(&*imageAvailableSemaphore) // Don't start until image is available + .setPWaitDstStageMask(waitStages) // Specifically wait before color output + .setCommandBufferCount(1) // Submit one command buffer + .setPCommandBuffers(&*commandBuffer) // The recorded rendering commands + .setSignalSemaphoreCount(1) // Signal one semaphore when complete + .setPSignalSemaphores(&*renderFinishedSemaphore); // Notify when rendering is finished + + // Submit work to GPU with fence-based completion tracking + // The fence allows CPU to know when this frame's GPU work has completed graphicsQueue.submit(1, &submitInfo, *inFlightFence); +---- + +The submission configuration demonstrates Vulkan's explicit synchronization model for coordinating multiple GPU operations. The wait semaphore ensures that rendering commands don't execute until the swapchain image is actually available for modification. The wait stage mask specifies exactly which part of the graphics pipeline must wait—in this case, color attachment output—allowing earlier pipeline stages to proceed if they don't depend on the swapchain image. - // Present the image +The signal semaphore communicates completion of rendering work to other operations that depend on the rendered result, such as presentation. The fence provides CPU-visible completion notification, enabling the frame pacing logic we saw in earlier. This three-way synchronization (wait semaphore, signal semaphore, and fence) creates a complete coordination system for the frame rendering pipeline. + +=== Frame Rendering: Presentation Coordination and Display Integration + +Finally, we coordinate with the presentation engine to display the rendered frame, ensuring that presentation waits for rendering completion and handles the transition from rendering to display. + +[source,cpp] +---- + // Present the rendered image to the display + // This operation transfers the completed frame from rendering to display system vk::PresentInfoKHR presentInfo; - presentInfo.setWaitSemaphoreCount(1) - .setPWaitSemaphores(&*renderFinishedSemaphore) - .setSwapchainCount(1) - .setPSwapchains(&*swapchain) - .setPImageIndices(&imageIndex); + presentInfo.setWaitSemaphoreCount(1) // Wait for rendering completion + .setPWaitSemaphores(&*renderFinishedSemaphore) // Don't present until rendering finishes + .setSwapchainCount(1) // Present to one swapchain + .setPSwapchains(&*swapchain) // Target swapchain for presentation + .setPImageIndices(&imageIndex); // Present the image we rendered to + // Submit presentation request to the presentation engine result = presentQueue.presentKHR(&presentInfo); } ---- +The presentation phase completes the frame rendering cycle by coordinating the transfer from rendering to display. The wait semaphore ensures that presentation doesn't begin until all rendering operations have completed, preventing the display of partially rendered frames. This synchronization is crucial because presentation and rendering may occur on different GPU queues with different timing characteristics. + +The presentation operation itself is asynchronous—it queues the presentation request and returns immediately, allowing the CPU to begin preparing the next frame. The presentation engine handles the actual coordination with the display hardware, including timing synchronization with refresh rates and managing the transition of the swapchain image from "rendering" to "displaying" to "available for reuse" states. + ==== How Rendergraphs Help with Synchronization Rendergraphs simplify synchronization by: @@ -596,90 +775,169 @@ When combined with rendergraphs, dynamic rendering becomes even more powerful. T ===== Example: Implementing a Deferred Renderer with a Rendergraph and Dynamic Rendering -Here's how you might implement a deferred renderer using a rendergraph with dynamic rendering: +Deferred rendering represents a sophisticated rendering technique that separates geometry processing from lighting calculations, enabling efficient handling of complex lighting scenarios. Let's examine how to implement this technique using rendergraphs and dynamic rendering through a comprehensive breakdown of the setup process. + +=== Deferred Renderer Setup: G-Buffer Resource Configuration + +First, we establish the G-Buffer (Geometry Buffer) resources that will store intermediate geometry information for the deferred lighting pass. [source,cpp] ---- +// Comprehensive deferred renderer setup demonstrating rendergraph resource management +// This implementation shows how to efficiently organize multi-pass rendering workflows void SetupDeferredRenderer(Rendergraph& graph, uint32_t width, uint32_t height) { - // Add resources + + // Configure position buffer for world-space vertex positions + // High precision format preserves positional accuracy for lighting calculations graph.AddResource("GBuffer_Position", vk::Format::eR16G16B16A16Sfloat, {width, height}, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + // Configure normal buffer for surface orientation data + // High precision normals enable accurate lighting and reflection calculations graph.AddResource("GBuffer_Normal", vk::Format::eR16G16B16A16Sfloat, {width, height}, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + // Configure albedo buffer for surface color information + // Standard 8-bit precision sufficient for color data with gamma encoding graph.AddResource("GBuffer_Albedo", vk::Format::eR8G8B8A8Unorm, {width, height}, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + // Configure depth buffer for occlusion and depth testing + // High precision depth enables accurate depth reconstruction in lighting pass graph.AddResource("Depth", vk::Format::eD32Sfloat, {width, height}, vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eInputAttachment, vk::ImageLayout::eUndefined, vk::ImageLayout::eDepthStencilAttachmentOptimal); + // Configure final color buffer for the completed lighting result + // Standard color format with transfer capability for presentation or post-processing graph.AddResource("FinalColor", vk::Format::eR8G8B8A8Unorm, {width, height}, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal); +---- + +The G-Buffer resource configuration represents the foundation of deferred rendering, where each buffer stores specific geometric information that will be consumed during lighting calculations. The format choices reflect a balance between precision requirements and memory efficiency: positions and normals use 16-bit floating point for accurate lighting calculations, while albedo uses 8-bit integers for color data where gamma correction naturally reduces precision requirements. - // Add geometry pass +The usage flag combinations enable each resource to serve dual roles: first as render targets during the geometry pass, then as input textures during the lighting pass. This dual usage pattern is characteristic of deferred rendering workflows, where the same data moves through multiple pipeline stages with different access patterns. + +=== Deferred Renderer Setup: Geometry Pass Configuration and Multiple Render Target Setup + +Next, we configure the geometry pass that populates the G-Buffer with geometric information from the scene's 3D models. + +[source,cpp] +---- + // Configure geometry pass for G-Buffer population + // This pass renders all geometry and stores intermediate data for lighting calculations graph.AddPass("GeometryPass", - {}, // No inputs - {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, + {}, // No input dependencies - first pass in pipeline + {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, // Outputs all G-Buffer components [&](vk::raii::CommandBuffer& cmd) { - // Begin dynamic rendering - std::array colorAttachments; - // Set up color attachments for G-buffer - // ... - - // Set up depth attachment - // ... + // Configure multiple render target attachments for G-Buffer output + // Each attachment corresponds to a different geometric property + std::array colorAttachments; + // Configure position attachment - world space vertex positions + colorAttachments[0].setImageView(/* GBuffer_Position view */) // Target position buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) // Optimal for writes + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to known state + .setStoreOp(vk::AttachmentStoreOp::eStore); // Preserve for lighting pass + + // Configure normal attachment - surface normals in world space + colorAttachments[1].setImageView(/* GBuffer_Normal view */) // Target normal buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to default normal + .setStoreOp(vk::AttachmentStoreOp::eStore); // Preserve for lighting + + // Configure albedo attachment - surface color and material properties + colorAttachments[2].setImageView(/* GBuffer_Albedo view */) // Target albedo buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to default color + .setStoreOp(vk::AttachmentStoreOp::eStore); // Preserve for lighting + + // Configure depth attachment for occlusion culling + vk::RenderingAttachmentInfoKHR depthAttachment; + depthAttachment.setImageView(/* Depth view */) // Target depth buffer + .setImageLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal) // Optimal for depth ops + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to far plane + .setStoreOp(vk::AttachmentStoreOp::eStore) // Preserve for lighting pass + .setClearValue({1.0f, 0}); // Clear to maximum depth + + // Assemble complete rendering configuration vk::RenderingInfoKHR renderingInfo; - renderingInfo.setRenderArea({{0, 0}, {width, height}}) - .setLayerCount(1) - .setColorAttachmentCount(colorAttachments.size()) - .setPColorAttachments(colorAttachments.data()) - .setPDepthAttachment(&depthAttachment); + renderingInfo.setRenderArea({{0, 0}, {width, height}}) // Full screen rendering + .setLayerCount(1) // Single layer rendering + .setColorAttachmentCount(colorAttachments.size()) // Number of G-Buffer targets + .setPColorAttachments(colorAttachments.data()) // G-Buffer attachment array + .setPDepthAttachment(&depthAttachment); // Depth testing configuration - cmd.beginRendering(renderingInfo); + // Execute geometry rendering with dynamic rendering + cmd.beginRendering(renderingInfo); // Begin G-Buffer population - // Bind pipeline and draw geometry - // ... + // Bind geometry pipeline and render all scene objects + // Each draw call populates position, normal, and albedo for visible fragments + // ... (geometry rendering implementation would go here) - cmd.endRendering(); + cmd.endRendering(); // Complete G-Buffer population }); +---- + +The geometry pass configuration demonstrates the power of deferred rendering's separation of concerns, where geometric complexity is handled independently of lighting complexity. The multiple render target setup enables simultaneous output to all G-Buffer components in a single rendering pass, maximizing GPU efficiency compared to multiple separate passes. + +The dynamic rendering approach eliminates the need to pre-configure render pass objects, providing flexibility to adjust G-Buffer formats or attachment counts based on runtime requirements. This flexibility is particularly valuable for techniques like adaptive quality settings or optional G-Buffer components for different material types. + +=== Deferred Renderer Setup: Lighting Pass Configuration and Screen-Space Processing - // Add lighting pass +Now we should set up the lighting pass that reads from the G-Buffer and performs all lighting calculations in screen space, producing the final rendered image. + +[source,cpp] +---- + // Configure lighting pass for screen-space illumination calculations + // This pass reads G-Buffer data and computes final lighting for each pixel graph.AddPass("LightingPass", - {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, - {"FinalColor"}, + {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, // Read all G-Buffer components + {"FinalColor"}, // Output final lit result [&](vk::raii::CommandBuffer& cmd) { - // Begin dynamic rendering - vk::RenderingAttachmentInfoKHR colorAttachment; - // Set up color attachment for final color - // ... + // Configure single color output for final lighting result + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(/* FinalColor view */) // Target final color buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) // Optimal for color writes + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to background color + .setStoreOp(vk::AttachmentStoreOp::eStore) // Preserve final result + .setClearValue({0.0f, 0.0f, 0.0f, 1.0f}); // Clear to black background + + // Configure lighting pass rendering without depth testing + // Depth testing unnecessary since we're processing each pixel exactly once vk::RenderingInfoKHR renderingInfo; - renderingInfo.setRenderArea({{0, 0}, {width, height}}) - .setLayerCount(1) - .setColorAttachmentCount(1) - .setPColorAttachments(&colorAttachment); + renderingInfo.setRenderArea({{0, 0}, {width, height}}) // Full screen processing + .setLayerCount(1) // Single layer output + .setColorAttachmentCount(1) // Single color output + .setPColorAttachments(&colorAttachment); // Final color attachment - cmd.beginRendering(renderingInfo); + // Execute screen-space lighting calculations + cmd.beginRendering(renderingInfo); // Begin lighting pass - // Bind pipeline and draw full-screen quad - // ... + // Bind lighting pipeline and draw full-screen quad + // Fragment shader reads G-Buffer textures and computes lighting for each pixel + // All scene lights are processed in a single screen-space pass + // ... (lighting calculation implementation would go here) - cmd.endRendering(); + cmd.endRendering(); // Complete lighting calculations }); - // Compile the rendergraph + // Compile the complete rendergraph for execution + // This analyzes dependencies and generates optimal execution plan graph.Compile(); } ---- +The lighting pass represents the core advantage of deferred rendering: decoupling lighting complexity from geometric complexity. By processing lighting in screen space, the cost becomes proportional to screen resolution rather than scene complexity, enabling efficient handling of scenes with many lights or complex lighting models. + +The single render target configuration reflects the unified nature of the lighting pass, where all lighting contributions are accumulated into the final color buffer. This approach enables advanced lighting techniques like physically-based rendering or global illumination algorithms that would be prohibitively expensive in forward rendering scenarios with complex geometry. + ==== Best Practices for Rendergraphs and Synchronization 1. *Minimize Synchronization*: Use the rendergraph to minimize the number of synchronization points. diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc index 05e742ff..8e6c4ab1 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Event Systems -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Event Systems @@ -38,6 +31,10 @@ When designing an event system, consider these principles: Let's implement a basic event system: +==== Base event type and convenience macro + +We start with a minimal base event interface and a helper macro to define strongly typed events without boilerplate. + [source,cpp] ---- // Base event class @@ -57,7 +54,16 @@ public: static const char* GetStaticType() { return #type; } \ virtual const char* GetType() const override { return GetStaticType(); } \ virtual Event* Clone() const override { return new type(*this); } +---- + +This lets us identify and copy events generically while keeping concrete event classes small. + +==== Concrete event types +Keep event payloads focused and lightweight; they should represent facts, not behavior. + +[source,cpp] +---- // Example event types class WindowResizeEvent : public Event { private: @@ -86,7 +92,14 @@ public: DEFINE_EVENT_TYPE(KeyPressEvent) }; +---- +==== Listener and type-safe dispatcher + +Listeners receive events; the dispatcher routes a generic Event to a typed handler when types match. + +[source,cpp] +---- // Event listener interface class EventListener { public: @@ -112,7 +125,14 @@ public: return false; } }; +---- +==== Event bus (immediate vs. queued) + +The bus can deliver immediately (low latency) or queue for later (deterministic ordering across frames). + +[source,cpp] +---- // Event bus class EventBus { private: diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc index ebafec83..6cdd1be0 100644 --- a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc +++ b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc @@ -1,13 +1,6 @@ :pp: {plus}{plus} = Engine Architecture: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion @@ -15,45 +8,39 @@ In this chapter, we've explored the fundamental architectural patterns and desig === What We've Covered -Throughout this chapter, we've delved into several key aspects of engine architecture: +This chapter has taken you through the foundational thinking that separates successful engine development from ad-hoc rendering code. We began by examining architectural patterns that have proven effective in production engines—layered architecture provides clear separation of concerns, component-based systems enable flexible object composition, data-oriented design unlocks performance potential, and service locators manage dependencies cleanly. Understanding these patterns helps you choose the right structural approach for different engine subsystems, balancing flexibility against complexity based on your specific needs. -1. *Architectural Patterns* - We examined common design patterns used in game and rendering engines, including layered architecture, component-based architecture, data-oriented design, and the service locator pattern. Each pattern offers different trade-offs in terms of flexibility, performance, and complexity. +The component system implementation demonstrated how composition can replace deep inheritance hierarchies, creating more maintainable and flexible code. This approach allows you to build diverse game objects by combining reusable components rather than creating complex class hierarchies that become difficult to extend and modify. -2. *Component Systems* - We implemented a flexible component-based architecture that allows for modular and reusable code. This approach promotes composition over inheritance, making it easier to create diverse game objects without deep inheritance hierarchies. +Resource management emerged as a critical foundation that affects every other system. Our implementation showcases how reference counting, caching, and hot reloading work together to optimize memory usage while improving development workflow. These techniques become essential as your projects scale beyond simple scenes to complex, asset-heavy applications. -3. *Resource Management* - We designed a robust resource management system that efficiently handles assets like textures, meshes, and shaders. Our system includes features such as reference counting, caching, and hot reloading to optimize memory usage and improve development workflow. +The rendering pipeline structure provides the framework for accommodating different rendering techniques and effects. By organizing stages for scene culling, render pass management, command generation, and post-processing, we've created a system that can evolve with your rendering needs without requiring fundamental architectural changes. -4. *Rendering Pipeline* - We structured a flexible rendering pipeline that can accommodate different rendering techniques and effects. Our pipeline includes stages for scene culling, render pass management, command generation, and post-processing. - -5. *Event Systems* - We implemented a decoupled communication system that allows different parts of the engine to interact without creating tight dependencies. Our event system supports features like event filtering, priorities, and bubbling. +Finally, the event system implementation shows how to enable communication between engine subsystems without creating tight coupling. Features like event filtering, priorities, and bubbling create a flexible communication layer that scales from simple notifications to complex interaction patterns. === Applying These Concepts -As you develop your own rendering engine, keep these principles in mind: - -1. *Start Simple* - Begin with a minimal implementation and add complexity as needed. It's easier to extend a simple, working system than to debug a complex one. +The transition from understanding architectural patterns to implementing them successfully requires a disciplined approach that balances ambition with pragmatism. Starting with minimal implementations provides a solid foundation you can build upon—complex architectures often hide subtle bugs that become exponentially harder to debug as system complexity increases. Each additional layer of abstraction should solve a concrete problem you've encountered, not anticipate hypothetical future needs. -2. *Focus on Interfaces* - Design clear interfaces between subsystems. This makes it easier to modify or replace individual components without affecting the rest of the engine. +Interface design becomes your most powerful tool for managing complexity as your engine grows. Well-defined interfaces act as contracts between subsystems, allowing you to modify or completely replace implementations without cascading changes throughout your codebase. This separation of concerns proves invaluable when optimizing performance, adding features, or adapting to new requirements. -3. *Consider Performance Early* - While premature optimization should be avoided, certain architectural decisions can have significant performance implications that are difficult to change later. +Performance considerations need to inform architectural decisions from the beginning, though this differs from premature optimization. Certain structural choices—like data layout patterns, memory allocation strategies, and inter-system communication mechanisms—create performance ceilings that become extremely expensive to change later. Understanding these implications helps you make informed trade-offs during initial design phases. -4. *Iterate and Refactor* - Your first design won't be perfect. Be prepared to refactor as you learn more about your specific requirements and constraints. +Successful engine development requires embracing iteration and refactoring as core activities rather than necessary evils. Your understanding of requirements will evolve as you build and use your engine, and rigid adherence to initial designs often leads to increasingly awkward workarounds. Regular refactoring keeps your architecture aligned with actual needs rather than theoretical ideals. -5. *Balance Flexibility and Complexity* - More flexible systems often come with increased complexity. Find the right balance for your project's needs. +The balance between flexibility and complexity represents perhaps the most challenging aspect of engine architecture. Every abstraction layer and configurable system adds cognitive overhead and potential failure points, but insufficient flexibility leads to brittle, hard-to-extend code. Finding the right balance depends on understanding your specific project constraints, team size, timeline, and performance requirements. === Next Steps -Throughout this tutorial, we've been building a simple engine together, exploring key concepts and implementing core components. As we continue through the tutorial, we'll build upon these architectural foundations. Here are some suggestions for enhancing your learning: - -1. *Follow Along with the Code* - As you progress through the tutorial, implement the code examples to build your engine step by step. Each section contains code snippets that you can use as a reference. +The architectural foundation we've established in this chapter will support everything we build in subsequent chapters. As we progress through camera systems, lighting, model loading, and advanced features, each new system will integrate with these core patterns rather than existing as isolated components. -2. *Experiment with Variations* - Once you understand a concept, try modifying the implementation to see how different approaches affect your engine. +Active implementation proves far more valuable than passive reading when learning engine architecture. Build the code examples as you encounter them, but don't stop there—experiment with variations to understand how different approaches affect your engine's behavior. This experimentation develops the intuitive understanding that separates competent engine developers from those who merely copy implementations. -3. *Explore Advanced Topics* - The link:../Appendix/appendix.adoc[Appendix] contains additional information on advanced rendering techniques and architectural patterns. +The architectural concepts we've covered provide a foundation, but production engines require additional sophistication. The link:../Appendix/appendix.adoc[Appendix] explores advanced rendering techniques and architectural patterns that build on these fundamentals, helping you understand how simple patterns scale to handle complex real-world requirements. -4. *Study Existing Engines* - Examine open-source engines like link:https://github.com/TheCherno/Hazel[Hazel] or link:https://github.com/LWJGL/lwjgl3[LWJGL] to see how they implement similar concepts. +Studying existing open-source engines like link:https://github.com/TheCherno/Hazel[Hazel] or examining the architectural decisions in established frameworks like link:https://github.com/LWJGL/lwjgl3[LWJGL] provides valuable perspective on how these concepts apply in practice. Look for patterns we've discussed and notice how different engines make different trade-offs based on their specific goals and constraints. -5. *Join the Community* - Engage with the graphics programming community to share ideas and get feedback on your implementation. +The graphics programming community offers tremendous value for learning and problem-solving. Engaging with forums, Discord servers, and GitHub discussions exposes you to diverse approaches and helps you get feedback on your architectural decisions. Often, discussing your implementation choices with others reveals assumptions you didn't realize you were making. Remember that engine development is an iterative process. Your architecture will evolve as you gain experience and as your requirements change. The concepts we've covered provide a foundation, but the best architecture for your engine will depend on your specific goals and constraints. From acfbd6eda78eec6f9d9a1cc6f1d80307ee1ed499 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 12:54:45 -0700 Subject: [PATCH 054/102] committing requested changes in stages. --- .../GUI/01_introduction.adoc | 40 +- .../GUI/02_imgui_setup.adoc | 395 ++++++++++++------ .../GUI/03_input_handling.adoc | 9 +- .../GUI/04_ui_elements.adoc | 26 +- .../GUI/05_vulkan_integration.adoc | 238 ++++++++--- .../GUI/06_conclusion.adoc | 9 +- en/Building_a_Simple_Engine/GUI/index.adoc | 9 +- 7 files changed, 482 insertions(+), 244 deletions(-) diff --git a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc index 77fe9a54..420f9ee6 100644 --- a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: Introduction -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Introduction @@ -15,28 +8,29 @@ Welcome to the "GUI" chapter of our "Building a Simple Engine" series! After imp In this chapter, we'll integrate a popular immediate-mode GUI library called Dear ImGui with our Vulkan engine. Dear ImGui is widely used in the game and graphics industry due to its simplicity, performance, and flexibility. It allows developers to quickly create debug interfaces, tools, and in-game menus without the complexity of traditional retained-mode GUI systems. -In this chapter, we'll focus on: +This chapter will guide you through integrating a professional GUI system into your Vulkan engine. We'll start by setting up Dear ImGui with Vulkan, establishing the foundation for all GUI functionality. The integration requires careful management of Vulkan resources—we'll create dedicated buffers, textures, and pipelines that work alongside your existing rendering systems without interference. -* Setting up Dear ImGui with Vulkan -* Creating and managing Vulkan resources for GUI rendering (buffers, textures, pipeline) -* Handling user input for both 3D navigation and GUI interaction -* Understanding key GUI integration concepts rather than exhaustive widget examples -* Integrating the GUI rendering with our Vulkan rendering pipeline -* Implementing object picking for 3D scene interaction +User input handling becomes more complex when you need to support both 3D scene navigation and GUI interaction. We'll implement a system that can distinguish between input intended for the 3D world and input meant for interface elements, ensuring smooth interaction with both. + +Rather than overwhelming you with exhaustive widget examples, we'll focus on the key integration concepts that enable GUI functionality. Understanding these principles will let you implement any interface elements your project needs. + +The rendering integration presents interesting challenges—your GUI needs to render on top of your 3D scene without disrupting the existing pipeline. We'll solve this by carefully managing render passes and ensuring proper depth testing and blending. + +Finally, we'll implement object picking, which bridges the gap between your GUI and 3D scene. This feature allows users to click on 3D objects and see their properties in the interface, creating a cohesive development environment. By the end of this chapter, you'll have a functional GUI system that you can use to control your camera, adjust rendering settings, and interact with your 3D scene. This will serve as a foundation for more advanced features in later chapters, such as material editors, scene hierarchies, and debugging tools. == Prerequisites -Before starting this chapter, you should have completed: +This chapter builds directly on the Camera & Transformations chapter, as we'll extend the camera system we developed there to work seamlessly with GUI interaction. The camera controls need to be aware of when the user is interacting with interface elements versus navigating the 3D scene. + +You'll also need a solid understanding of several core Vulkan concepts. The rendering pipeline and command buffer knowledge is crucial because GUI rendering requires careful coordination with your existing 3D rendering—we'll be recording GUI draw calls into the same command buffers while managing different pipeline states. + +Buffer and image creation skills are essential since Dear ImGui requires dedicated vertex and index buffers for its geometry, plus texture resources for fonts and any custom UI textures. Understanding descriptor sets and layouts becomes important as we'll need to create descriptors specifically for GUI rendering that don't interfere with your 3D scene descriptors. + +Pipeline creation knowledge ties everything together, as we'll build a specialized graphics pipeline for GUI rendering with different vertex input, shaders, and render state than your 3D pipeline. -* The Camera & Transformations chapter, as we'll build upon the camera system we developed there -* Solid understanding of Vulkan concepts, particularly: - ** Rendering pipeline and command buffers - ** Buffer and image creation - ** Descriptor sets and layouts - ** Pipeline creation -* Basic understanding of input handling concepts +A basic understanding of input handling concepts will help you follow along as we implement the dual-mode input system that can distinguish between 3D navigation and GUI interaction. Let's begin by exploring how to implement a professional GUI system with Dear ImGui and Vulkan. diff --git a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc index 1a5885ba..888195e1 100644 --- a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc +++ b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: Setting Up Dear ImGui -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Setting Up Dear ImGui @@ -69,7 +62,11 @@ target_include_directories(VulkanApp PRIVATE include) Let's implement the ImGuiVulkanUtil class to handle the integration between ImGui and Vulkan. -Let's start by defining our ImGuiVulkanUtil class: +The ImGuiVulkanUtil class serves as the bridge between ImGui's immediate-mode GUI system and Vulkan's explicit graphics API. This integration requires careful management of GPU resources, synchronization, and rendering state to efficiently display user interface elements alongside our 3D graphics. Let's break down the class architecture into logical components to understand how each part contributes to the overall integration. + +=== ImGuiVulkanUtil Architecture: GPU Resource Management Foundation + +First, we establish the core Vulkan resources needed to render ImGui's dynamically generated UI geometry on the GPU. [source,cpp] ---- @@ -81,73 +78,121 @@ Let's start by defining our ImGuiVulkanUtil class: class ImGuiVulkanUtil { private: - // Vulkan resources for rendering the UI - vk::raii::Sampler sampler{nullptr}; - Buffer vertexBuffer; - Buffer indexBuffer; - uint32_t vertexCount = 0; - uint32_t indexCount = 0; - Image fontImage; - ImageView fontImageView; - vk::raii::PipelineCache pipelineCache{nullptr}; - vk::raii::PipelineLayout pipelineLayout{nullptr}; - vk::raii::Pipeline pipeline{nullptr}; - vk::raii::DescriptorPool descriptorPool{nullptr}; - vk::raii::DescriptorSetLayout descriptorSetLayout{nullptr}; - vk::raii::DescriptorSet descriptorSet{nullptr}; - - // Device references - vk::raii::Device* device = nullptr; - vk::raii::PhysicalDevice* physicalDevice = nullptr; - vk::raii::Queue* graphicsQueue = nullptr; - uint32_t graphicsQueueFamily = 0; - - // UI style - ImGuiStyle vulkanStyle; - - // Push constants for UI rendering + // Core GPU rendering resources for UI display + // These objects form the foundation of our ImGui-to-Vulkan rendering pipeline + vk::raii::Sampler sampler{nullptr}; // Texture sampling configuration for font rendering + Buffer vertexBuffer; // Dynamic vertex buffer for UI geometry + Buffer indexBuffer; // Dynamic index buffer for UI triangle connectivity + uint32_t vertexCount = 0; // Current vertex count for draw commands + uint32_t indexCount = 0; // Current index count for draw commands + Image fontImage; // GPU texture containing ImGui font atlas + ImageView fontImageView; // Shader-accessible view of font texture +---- + +The GPU resource foundation reflects ImGui's dynamic rendering model, where UI geometry is generated fresh each frame based on the current interface layout. The vertex and index buffers use host-visible memory to enable efficient CPU updates, while the font texture remains static once loaded. This hybrid approach balances the need for dynamic UI updates with the performance benefits of GPU-resident font data. + +The buffer sizing strategy must accommodate ImGui's variable geometry output, which can change dramatically based on UI complexity. Unlike static 3D models, ImGui generates different amounts of geometry each frame, requiring our buffers to resize dynamically or be pre-allocated with sufficient capacity for worst-case scenarios. + +=== ImGuiVulkanUtil Architecture: Vulkan Pipeline Infrastructure + +Next, we set up the Vulkan pipeline objects that define how UI geometry is processed and rendered by the GPU. + +[source,cpp] +---- + // Vulkan pipeline infrastructure for UI rendering + // These objects define the complete GPU processing pipeline for ImGui elements + vk::raii::PipelineCache pipelineCache{nullptr}; // Pipeline compilation cache for faster startup + vk::raii::PipelineLayout pipelineLayout{nullptr}; // Resource binding layout (textures, uniforms) + vk::raii::Pipeline pipeline{nullptr}; // Complete graphics pipeline for UI rendering + vk::raii::DescriptorPool descriptorPool{nullptr}; // Pool for allocating descriptor sets + vk::raii::DescriptorSetLayout descriptorSetLayout{nullptr}; // Layout defining shader resource bindings + vk::raii::DescriptorSet descriptorSet{nullptr}; // Actual resource bindings for font texture +---- + +The pipeline infrastructure creates a specialized graphics pipeline optimized for UI rendering, which differs significantly from typical 3D rendering pipelines. UI rendering typically requires alpha blending for transparency effects, operates in 2D screen space rather than 3D world space, and uses simpler shading models focused on texture sampling rather than complex lighting calculations. + +The descriptor system manages the connection between our CPU-side resources and the GPU shaders. For UI rendering, this primarily involves binding the font atlas texture to the fragment shader, though more complex UI systems might include additional textures for icons, backgrounds, or other visual elements. + +=== ImGuiVulkanUtil Architecture: Device Context and System Integration + +Then, we maintain references to the Vulkan device context and manage integration with the broader graphics system. + +[source,cpp] +---- + // Vulkan device context and system integration + // These references connect our UI system to the broader Vulkan application context + vk::raii::Device* device = nullptr; // Primary Vulkan device for resource creation + vk::raii::PhysicalDevice* physicalDevice = nullptr; // GPU hardware info for capability queries + vk::raii::Queue* graphicsQueue = nullptr; // Command submission queue for UI rendering + uint32_t graphicsQueueFamily = 0; // Queue family index for validation +---- + +The device context integration demonstrates the explicit nature of Vulkan's resource management, where every operation requires specific device and queue references. Unlike higher-level graphics APIs that maintain global state, Vulkan requires explicit specification of which GPU device and command queue should handle each operation. + +The queue family index enables validation and optimization by ensuring that UI rendering operations use compatible queue types. While UI rendering typically uses the same graphics queue as 3D rendering, some applications might benefit from dedicated queues for different rendering responsibilities. + +=== ImGuiVulkanUtil Architecture: UI State and Rendering Configuration + +After that, we manage UI-specific state including styling, rendering parameters, and dynamic update tracking. + +[source,cpp] +---- + // UI state management and rendering configuration + // These members control the visual appearance and dynamic behavior of the UI system + ImGuiStyle vulkanStyle; // Custom visual styling for Vulkan applications + + // Push constants for efficient per-frame parameter updates + // This structure enables fast updates of transformation and styling data struct PushConstBlock { - glm::vec2 scale; - glm::vec2 translate; + glm::vec2 scale; // UI scaling factors for different screen sizes + glm::vec2 translate; // Translation offset for UI positioning } pushConstBlock; - // Flag to track if buffers need updating - bool needsUpdateBuffers = false; + // Dynamic state tracking for performance optimization + bool needsUpdateBuffers = false; // Flag indicating buffer resize requirements - // Pipeline state for dynamic rendering - vk::PipelineRenderingCreateInfo renderingInfo{}; - vk::Format colorFormat = vk::Format::eB8G8R8A8Unorm; + // Modern Vulkan rendering configuration + vk::PipelineRenderingCreateInfo renderingInfo{}; // Dynamic rendering setup parameters + vk::Format colorFormat = vk::Format::eB8G8R8A8Unorm; // Target framebuffer format +---- -public: - ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDevice& physicalDevice, - vk::raii::Queue& graphicsQueue, uint32_t graphicsQueueFamily); - ~ImGuiVulkanUtil(); +The styling and configuration management reflects ImGui's flexibility in visual presentation while maintaining compatibility with Vulkan's explicit rendering model. The push constants provide an efficient mechanism for updating per-frame parameters like screen resolution changes or UI scaling factors without requiring descriptor set updates. - // Initialize ImGui context and style - void init(float width, float height); +The dynamic state tracking optimizes performance by avoiding unnecessary GPU resource updates when the UI layout remains stable between frames. This optimization becomes particularly important in applications with complex UIs where buffer updates could otherwise impact frame rates. - // Initialize all Vulkan resources - void initResources(); +=== ImGuiVulkanUtil Architecture: Public Interface and Lifecycle Management - // Set UI style - void setStyle(uint32_t index); +Finally, we define the external interface that applications use to integrate ImGui rendering into their Vulkan rendering pipeline. - // Start a new ImGui frame - bool newFrame(); +[source,cpp] +---- +public: + // Lifecycle management for proper resource initialization and cleanup + ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Queue& graphicsQueue, uint32_t graphicsQueueFamily); + ~ImGuiVulkanUtil(); - // Update vertex and index buffers - void updateBuffers(); + // Core functionality methods for ImGui integration + void init(float width, float height); // Initialize ImGui context and configure display + void initResources(); // Create all Vulkan resources for rendering + void setStyle(uint32_t index); // Apply visual styling themes - // Draw ImGui elements to command buffer - void drawFrame(vk::raii::CommandBuffer& commandBuffer); + // Frame-by-frame rendering operations + bool newFrame(); // Begin new ImGui frame and generate geometry + void updateBuffers(); // Upload updated geometry to GPU buffers + void drawFrame(vk::raii::CommandBuffer& commandBuffer); // Record rendering commands to command buffer - // Input handling - void handleKey(int key, int scancode, int action, int mods); - bool getWantKeyCapture(); - void charPressed(uint32_t key); + // Input event handling for interactive UI elements + void handleKey(int key, int scancode, int action, int mods); // Process keyboard input events + bool getWantKeyCapture(); // Query if ImGui wants keyboard focus + void charPressed(uint32_t key); // Handle character input for text widgets }; ---- +The public interface design balances ease of integration with performance considerations, separating one-time setup operations from per-frame rendering tasks. The initialization methods handle the expensive resource creation that should happen once during application startup, while the frame-by-frame methods focus on efficient updates and rendering. + +The input handling interface enables proper integration with existing input systems, allowing ImGui to capture relevant events while passing through others to the main application. This cooperative approach ensures that UI elements can respond to user interaction without interfering with 3D scene controls or other input handling. + === Implementing the ImGuiVulkanUtil Class Now let's implement the methods of our ImGuiVulkanUtil class for the Vulkan implementation. @@ -247,111 +292,171 @@ void ImGuiVulkanUtil::setStyle(uint32_t index) { ==== Resource Initialization -Now let's implement the method to initialize all Vulkan resources needed for ImGui rendering: +Now let's implement the method to initialize all Vulkan resources needed for ImGui rendering. This complex process involves several distinct steps that work together to create the GPU resources required for text and UI rendering. + +=== Resource Initialization: Font Data Extraction and Memory Calculation + +First extract font atlas data from ImGui and calculates the memory requirements for GPU storage. [source,cpp] ---- void ImGuiVulkanUtil::initResources() { - // Create font texture + // Extract font atlas data from ImGui's internal font system + // ImGui generates a texture atlas containing all glyphs needed for text rendering ImGuiIO& io = ImGui::GetIO(); - unsigned char* fontData; - int texWidth, texHeight; + unsigned char* fontData; // Raw pixel data from font atlas + int texWidth, texHeight; // Dimensions of the generated font atlas io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight); + + // Calculate total memory requirements for GPU transfer + // Each pixel contains 4 bytes (RGBA) requiring precise memory allocation vk::DeviceSize uploadSize = texWidth * texHeight * 4 * sizeof(char); +---- + +The font data extraction represents the bridge between ImGui's CPU-based text rendering system and Vulkan's GPU-based texture pipeline. ImGui automatically generates a font atlas that combines all required character glyphs into a single texture, optimizing GPU memory usage and reducing draw calls during text rendering. The RGBA32 format provides full color and alpha support for anti-aliased text rendering. + +=== Resource Initialization: GPU Image Creation and Memory Allocation + +Next, create the GPU image resources that will store the font texture data in video memory. - // Create the font image +[source,cpp] +---- + // Define image dimensions and create extent structure + // Vulkan requires explicit specification of all image dimensions vk::Extent3D fontExtent{ - static_cast(texWidth), - static_cast(texHeight), - 1 + static_cast(texWidth), // Image width in pixels + static_cast(texHeight), // Image height in pixels + 1 // Single layer (not a 3D texture or array) }; - // Create image for font texture + // Create optimized GPU image for font texture storage + // This image will be sampled by shaders during UI rendering fontImage = Image(*device, fontExtent, vk::Format::eR8G8B8A8Unorm, vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal); - // Create image view + // Create image view for shader access + // The image view defines how shaders interpret the raw image data fontImageView = ImageView(*device, fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, vk::ImageAspectFlagBits::eColor); +---- + +The GPU image creation step establishes the foundation for efficient text rendering by allocating device-local memory that provides optimal access speeds for the GPU. The dual usage flags (eSampled | eTransferDst) enable both data upload operations and shader sampling, while the RGBA8_UNORM format ensures consistent color representation across different GPU architectures. + +=== Resource Initialization — Staging Buffer Creation and Data Transfer + +Next, we create a temporary staging buffer and transfer the font data from CPU memory to GPU memory. - // Upload font data to the image +[source,cpp] +---- + // Create staging buffer for efficient CPU-to-GPU data transfer + // Host-visible memory allows direct CPU access for data upload Buffer stagingBuffer(*device, uploadSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); - void* data = stagingBuffer.map(); - memcpy(data, fontData, uploadSize); - stagingBuffer.unmap(); + // Map staging buffer memory and copy font data + // Direct memory mapping provides the fastest path for data transfer + void* data = stagingBuffer.map(); // Map GPU memory to CPU address space + memcpy(data, fontData, uploadSize); // Copy font atlas data to GPU memory + stagingBuffer.unmap(); // Unmap memory to ensure data consistency +---- + +The staging buffer approach represents the most efficient method for transferring large amounts of data from CPU to GPU memory in Vulkan. Host-visible memory enables direct CPU access while host-coherent ensures that CPU writes are immediately visible to the GPU without requiring explicit cache flushes. This intermediate step is necessary because device-local memory (where the final image resides) is typically not directly accessible by the CPU. + +=== Resource Initialization — Image Layout Transitions and Data Upload + +Then, we manage the image layout transitions required for safe data transfer in Vulkan's explicit synchronization model. - // Transition image layout and copy data +[source,cpp] +---- + // Transition image to optimal layout for data reception + // Vulkan requires explicit layout transitions for optimal performance and correctness transitionImageLayout(fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + // Execute the actual buffer-to-image copy operation + // This transfers font data from staging buffer to the final GPU image copyBufferToImage(stagingBuffer.getHandle(), fontImage.getHandle(), static_cast(texWidth), static_cast(texHeight)); + // Transition image to shader-readable layout for rendering + // Final layout optimization enables efficient sampling during UI rendering transitionImageLayout(fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); +---- - // Create sampler for font texture - vk::SamplerCreateInfo samplerInfo{}; - samplerInfo.magFilter = vk::Filter::eLinear; - samplerInfo.minFilter = vk::Filter::eLinear; - samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; - samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; - samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; - samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; - samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; +The layout transition sequence ensures that the GPU memory subsystem can optimize its internal data arrangements for each operation type. The eTransferDstOptimal layout provides the best performance for receiving data uploads, while eShaderReadOnlyOptimal enables efficient texture sampling during rendering. These transitions include automatic memory barriers that synchronize access between different GPU pipeline stages. + +=== Resource Initialization — Texture Sampling Configuration and Descriptor Management - sampler = device->createSampler(samplerInfo); +Finally, we create the sampling configuration and descriptor resources needed for shader access to the font texture. - // Create descriptor pool +[source,cpp] +---- + // Configure texture sampling parameters for optimal text rendering + // These settings directly impact text quality and performance + vk::SamplerCreateInfo samplerInfo{}; + samplerInfo.magFilter = vk::Filter::eLinear; // Smooth scaling when magnified + samplerInfo.minFilter = vk::Filter::eLinear; // Smooth scaling when minified + samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; // Smooth transitions between mip levels + samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; // Prevent texture wrapping + samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; // Clean edge handling + samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; // 3D consistency + samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; // White border for clamped areas + + sampler = device->createSampler(samplerInfo); // Create the GPU sampler object + + // Create descriptor pool for shader resource binding + // Descriptors provide the interface between shaders and GPU resources vk::DescriptorPoolSize poolSize{vk::DescriptorType::eCombinedImageSampler, 1}; vk::DescriptorPoolCreateInfo poolInfo{}; - poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; - poolInfo.maxSets = 2; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; // Allow individual descriptor set freeing + poolInfo.maxSets = 2; // Maximum number of descriptor sets + poolInfo.poolSizeCount = 1; // Number of pool size specifications + poolInfo.pPoolSizes = &poolSize; // Pool size configuration - descriptorPool = device->createDescriptorPool(poolInfo); + descriptorPool = device->createDescriptorPool(poolInfo); // Create descriptor pool - // Create descriptor set layout + // Create descriptor set layout defining shader resource interface + // This layout must match the binding declarations in the ImGui shaders vk::DescriptorSetLayoutBinding binding{}; - binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; - binding.descriptorCount = 1; - binding.stageFlags = vk::ShaderStageFlagBits::eFragment; - binding.binding = 0; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; // Combined texture and sampler + binding.descriptorCount = 1; // Single texture binding + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; // Used in fragment shader + binding.binding = 0; // Shader binding point 0 vk::DescriptorSetLayoutCreateInfo layoutInfo{}; - layoutInfo.bindingCount = 1; - layoutInfo.pBindings = &binding; + layoutInfo.bindingCount = 1; // Number of bindings in layout + layoutInfo.pBindings = &binding; // Binding configuration array - descriptorSetLayout = device->createDescriptorSetLayout(layoutInfo); + descriptorSetLayout = device->createDescriptorSetLayout(layoutInfo); // Create layout object - // Allocate descriptor set + // Allocate descriptor set from pool using the defined layout + // This creates the actual binding that connects GPU resources to shaders vk::DescriptorSetAllocateInfo allocInfo{}; - allocInfo.descriptorPool = *descriptorPool; - allocInfo.descriptorSetCount = 1; - vk::DescriptorSetLayout layouts[] = {*descriptorSetLayout}; - allocInfo.pSetLayouts = layouts; + allocInfo.descriptorPool = *descriptorPool; // Source pool for allocation + allocInfo.descriptorSetCount = 1; // Number of sets to allocate + vk::DescriptorSetLayout layouts[] = {*descriptorSetLayout}; // Layout template array + allocInfo.pSetLayouts = layouts; // Layout configuration - descriptorSet = std::move(device->allocateDescriptorSets(allocInfo).front()); + descriptorSet = std::move(device->allocateDescriptorSets(allocInfo).front()); // Allocate and store set - // Update descriptor set + // Update descriptor set with actual font texture and sampler resources + // This final step connects the physical GPU resources to the shader binding points vk::DescriptorImageInfo imageInfo{}; - imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; - imageInfo.imageView = fontImageView.getHandle(); - imageInfo.sampler = *sampler; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; // Expected image layout + imageInfo.imageView = fontImageView.getHandle(); // Font texture view + imageInfo.sampler = *sampler; // Texture sampler vk::WriteDescriptorSet writeSet{}; - writeSet.dstSet = *descriptorSet; - writeSet.descriptorCount = 1; - writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; - writeSet.pImageInfo = &imageInfo; - writeSet.dstBinding = 0; + writeSet.dstSet = *descriptorSet; // Target descriptor set + writeSet.descriptorCount = 1; // Number of resources to bind + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; // Resource type + writeSet.pImageInfo = &imageInfo; // Image resource information + writeSet.dstBinding = 0; // Binding point in shader - device->updateDescriptorSets(1, &writeSet, 0, nullptr); + device->updateDescriptorSets(1, &writeSet, 0, nullptr); // Execute the binding update // Create pipeline cache vk::PipelineCacheCreateInfo pipelineCacheInfo{}; @@ -463,6 +568,12 @@ void ImGuiVulkanUtil::updateBuffers() { indexBuffer.unmap(); } +==== Begin a rendering scope + +Before issuing any UI draw commands, we open a dynamic rendering scope that targets the current framebuffer. This replaces vkCmdBeginRenderPass/EndRenderPass and keeps the UI pass lightweight. + +[source,cpp] +---- void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { ImDrawData* drawData = ImGui::GetDrawData(); if (!drawData || drawData->CmdListsCount == 0) { @@ -471,7 +582,7 @@ void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { // Begin dynamic rendering vk::RenderingAttachmentInfo colorAttachment{}; - // Note: In a real implementation, you would set the imageView, imageLayout, + // Note: In a real implementation, you would set imageView, imageLayout, // loadOp, storeOp, and clearValue based on your swapchain image vk::RenderingInfo renderingInfo{}; @@ -482,31 +593,56 @@ void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { renderingInfo.pColorAttachments = &colorAttachment; commandBuffer.beginRendering(renderingInfo); +---- + +At this point, commands affect the UI overlay only. Next we bind state that doesn’t change per draw. + +==== Bind pipeline and set viewport - // Bind the pipeline +[source,cpp] +---- + // Bind the pipeline used for ImGui commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pipeline); - // Set viewport + // Configure viewport for UI pixel coordinates vk::Viewport viewport{}; viewport.width = drawData->DisplaySize.x; viewport.height = drawData->DisplaySize.y; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; commandBuffer.setViewport(0, viewport); +---- - // Set push constants +The pipeline has blending and raster states tailored for UI. The viewport maps ImGui’s coordinate system to the framebuffer. + +==== Push per-frame constants + +[source,cpp] +---- + // Convert from ImGui coordinates into NDC via a simple scale/translate pushConstBlock.scale = glm::vec2(2.0f / drawData->DisplaySize.x, 2.0f / drawData->DisplaySize.y); pushConstBlock.translate = glm::vec2(-1.0f); commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(PushConstBlock), &pushConstBlock); +---- + +This keeps the shader simple and avoids per-vertex work for coordinate transforms. - // Bind vertex and index buffers +==== Bind geometry buffers + +[source,cpp] +---- + // We already filled these buffers this frame vk::Buffer vertexBuffers[] = { vertexBuffer.getHandle() }; vk::DeviceSize offsets[] = { 0 }; commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); commandBuffer.bindIndexBuffer(indexBuffer.getHandle(), 0, vk::IndexType::eUint16); +---- + +==== Iterate command lists, set scissor, draw - // Render command lists +[source,cpp] +---- int vertexOffset = 0; int indexOffset = 0; @@ -516,7 +652,7 @@ void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { for (int j = 0; j < cmdList->CmdBuffer.Size; j++) { const ImDrawCmd* pcmd = &cmdList->CmdBuffer[j]; - // Set scissor rectangle + // Clip per draw call vk::Rect2D scissor{}; scissor.offset.x = std::max(static_cast(pcmd->ClipRect.x), 0); scissor.offset.y = std::max(static_cast(pcmd->ClipRect.y), 0); @@ -524,19 +660,26 @@ void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { scissor.extent.height = static_cast(pcmd->ClipRect.w - pcmd->ClipRect.y); commandBuffer.setScissor(0, scissor); - // Bind descriptor set (font texture) + // Bind font (and any UI) textures for this draw commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, *descriptorSet, {}); - // Draw + // Issue indexed draw for this UI batch commandBuffer.drawIndexed(pcmd->ElemCount, 1, indexOffset, vertexOffset, 0); indexOffset += pcmd->ElemCount; } vertexOffset += cmdList->VtxBuffer.Size; } +---- + +Each ImDrawCmd provides a scissor rect that clips widgets efficiently without extra passes. - // End dynamic rendering +==== End the rendering scope + +[source,cpp] +---- + // Close the rendering scope for the UI overlay commandBuffer.endRendering(); } ---- diff --git a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc index 313a8cae..b28760cc 100644 --- a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc +++ b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: Input Handling -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Input Handling diff --git a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc index 3c70791a..6706e8c9 100644 --- a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc +++ b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: UI Elements and Integration Concepts -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == UI Elements and Integration Concepts @@ -184,7 +177,8 @@ To display a Vulkan texture in ImGui, you need to: 3. Update the descriptor set with your texture's image view and sampler 4. Pass the descriptor set handle to ImGui -Here's how to set up the necessary resources: +===== Create the descriptor set layout +This layout declares a single combined image sampler the shader can sample from when ImGui draws the quad. [source,cpp] ---- @@ -200,7 +194,13 @@ layoutInfo.bindingCount = 1; layoutInfo.pBindings = &binding; vk::raii::DescriptorSetLayout textureSetLayout = device.createDescriptorSetLayout(layoutInfo); +---- + +===== Allocate a descriptor set +Allocate one set per texture you want to show in ImGui. +[source,cpp] +---- // Allocate a descriptor set for each texture vk::DescriptorSetAllocateInfo allocInfo{}; allocInfo.descriptorPool = *descriptorPool; @@ -209,7 +209,13 @@ vk::DescriptorSetLayout layouts[] = {*textureSetLayout}; allocInfo.pSetLayouts = layouts; vk::raii::DescriptorSet textureDescriptorSet = std::move(device.allocateDescriptorSets(allocInfo).front()); +---- + +===== Update the descriptor set +Point the descriptor at your image view and sampler in shader‑read layout. +[source,cpp] +---- // Update the descriptor set with your texture vk::DescriptorImageInfo imageInfo{}; imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; @@ -226,7 +232,7 @@ writeSet.dstBinding = 0; device.updateDescriptorSets(1, &writeSet, 0, nullptr); ---- -==== Using Textures in ImGui +==== Use it in ImGui Once you have set up the descriptor set, you can use it with ImGui's image functions: diff --git a/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc index 6b64c5b3..8bf28eca 100644 --- a/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: Vulkan Integration -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Vulkan Integration @@ -64,49 +57,110 @@ Let's look at both approaches: This is the simplest approach and works well for most applications. With dynamic rendering, the code becomes even cleaner: +=== Command Buffer Initialization + +The frame rendering process begins with command buffer preparation, where we set up the recording state and prepare for GPU command submission. + [source,cpp] ---- void drawFrame() { // ... existing frame preparation code ... - // Start recording command buffer + // Initialize command buffer recording + // This tells Vulkan we're about to record a sequence of GPU commands vk::CommandBufferBeginInfo beginInfo{}; commandBuffer.begin(beginInfo); +---- - // Begin dynamic rendering for scene - vk::RenderingAttachmentInfo colorAttachment{}; - colorAttachment.imageView = *swapChainImageViews[imageIndex]; - colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; - colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; - colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; - colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; +Command buffer recording represents the heart of Vulkan's explicit GPU control model. Unlike older APIs where rendering commands are immediately submitted to the GPU, Vulkan allows us to build up a complete sequence of operations before submission. This approach enables powerful optimizations like command reordering, parallel command buffer construction, and efficient GPU scheduling. + +The 'begin' operation transitions the command buffer from an initial state into a recording state, where subsequent API calls will be captured as GPU instructions rather than executed immediately. This explicit state management gives us precise control over when and how GPU work is submitted, enabling the fine-grained performance control that makes Vulkan so powerful for demanding applications. +=== Dynamic Rendering Attachment Setup + +Dynamic rendering requires us to explicitly describe our render targets and their properties, replacing the traditional render pass system with a more flexible approach. + +[source,cpp] +---- + // Configure color attachment for the main render target + // This describes how the GPU should handle the color output + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; // Target swapchain image + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; // Optimal layout for color output + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear the image before rendering + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; // Preserve results after rendering + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; // Clear to black + + // Configure depth attachment for 3D depth testing + // This enables proper occlusion and depth sorting for 3D objects vk::RenderingAttachmentInfo depthAttachment{}; - depthAttachment.imageView = *depthImageView; - depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; - depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; - depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; - depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + depthAttachment.imageView = *depthImageView; // Depth buffer image + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; // Optimal for depth operations + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear depth buffer to far plane + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; // Don't preserve depth after rendering + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; // Clear to maximum depth +---- - vk::RenderingInfo renderingInfo{}; - renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; - renderingInfo.layerCount = 1; - renderingInfo.colorAttachmentCount = 1; - renderingInfo.pColorAttachments = &colorAttachment; - renderingInfo.pDepthAttachment = &depthAttachment; +The attachment configuration system provides explicit control over how the GPU handles our render targets throughout the rendering process. By specifying load and store operations, we can optimize memory bandwidth by only preserving data that needs to carry forward to subsequent passes. The clear operations ensure we start with a known state, preventing visual artifacts from previous frame data. +Image layout transitions happen automatically based on our specifications, with the GPU driver handling the necessary memory barriers and cache flushes to ensure data coherency. The optimal layouts we specify here tell the driver to arrange the image data in whatever format provides the best performance for the intended usage, rather than forcing a specific memory organization. + +=== Dynamic Rendering Pass Setup + +With our attachments configured, we now assemble them into a complete rendering pass that describes the full rendering operation to the GPU. + +[source,cpp] +---- + // Assemble the complete rendering operation description + // This ties together all our attachments and rendering parameters + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; // Render to entire swapchain area + renderingInfo.layerCount = 1; // Single layer (not array rendering) + renderingInfo.colorAttachmentCount = 1; // One color output + renderingInfo.pColorAttachments = &colorAttachment; // Our configured color attachment + renderingInfo.pDepthAttachment = &depthAttachment; // Our configured depth attachment + + // Begin the dynamic rendering pass + // This establishes the rendering context for subsequent draw commands commandBuffer.beginRendering(renderingInfo); +---- + +Dynamic rendering represents a significant evolution from traditional Vulkan render passes, providing greater flexibility while maintaining the performance benefits of explicit GPU control. Instead of pre-defining render pass objects at initialization time, we can specify render targets and their properties at command recording time, enabling more dynamic and flexible rendering architectures. + +The render area specification allows for partial-screen rendering, which can provide significant performance benefits when only portions of the screen need updating. For full-screen rendering like our case, we specify the entire swapchain extent to ensure complete coverage. - // Render 3D scene +=== 3D Scene Rendering + +The main scene rendering phase handles all 3D geometry, lighting, and material rendering within the established rendering context. + +[source,cpp] +---- + // Execute 3D scene rendering + // All your existing 3D geometry, lighting, and material rendering happens here // ... your existing scene rendering code ... + // Complete the 3D rendering pass + // This finalizes all 3D rendering operations and prepares for UI overlay commandBuffer.endRendering(); +---- + +The scene rendering phase operates within the rendering context we established, with the GPU automatically handling depth testing, color blending, and other rasterization operations according to our pipeline configurations. All draw commands issued between beginRendering and endRendering will target our configured attachments with the specified clear and store behaviors. + +The explicit endRendering call ensures that all scene rendering operations are properly completed and that render targets are transitioned to appropriate states for subsequent operations. This explicit control allows the GPU driver to perform optimal scheduling and memory management for the rendering workload. + +=== UI Overlay Integration - // Render ImGui using our custom renderer - // ImGui will handle its own dynamic rendering internally +The final rendering phase integrates ImGui UI elements as an overlay on top of the 3D scene, requiring careful coordination between the two rendering systems. + +[source,cpp] +---- + // Render ImGui UI overlay on top of the 3D scene + // The custom renderer handles ImGui's own dynamic rendering setup internally + // This includes vertex buffer uploads, pipeline binding, and draw command generation renderer.render(ImGui::GetDrawData(), commandBuffer); - // End command buffer + // Finalize command buffer recording + // This transitions the command buffer to executable state for GPU submission commandBuffer.end(); // Submit command buffer @@ -118,70 +172,132 @@ void drawFrame() { This approach gives you more flexibility and can be useful for more complex rendering pipelines. With dynamic rendering, it becomes even more straightforward: +=== Multi-Buffer: Scene Command Buffer Recording + +The multiple command buffer approach begins by isolating 3D scene rendering into its own dedicated command buffer, providing greater flexibility for complex rendering pipelines. + [source,cpp] ---- void drawFrame() { // ... existing frame preparation code ... - // Record scene command buffer + // Initialize scene-specific command buffer recording + // This dedicated buffer will contain only 3D geometry and lighting operations vk::CommandBufferBeginInfo beginInfo{}; sceneCommandBuffer.begin(beginInfo); +---- - // Begin dynamic rendering for scene - vk::RenderingAttachmentInfo colorAttachment{}; - colorAttachment.imageView = *swapChainImageViews[imageIndex]; - colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; - colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; - colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; - colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; +Separating scene rendering into its own command buffer provides several architectural advantages. First, it enables parallel command buffer recording where different threads can simultaneously build scene and UI command sequences, improving CPU utilization on multi-core systems. Second, it allows for independent optimization of each rendering phase, where scene rendering can use different GPU queues or submission timing than UI rendering. + +This separation also facilitates advanced rendering techniques like multi-frame latency optimization, where scene rendering can be decoupled from UI updates to maintain consistent frame timing even when one system experiences performance variations. + +=== Multi-Buffer: Scene Attachment Configuration +The scene rendering setup mirrors the single-buffer approach but with explicit ownership of the attachment configuration within the scene command buffer. + +[source,cpp] +---- + // Configure scene rendering attachments with explicit ownership + // These configurations belong specifically to the scene rendering pass + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; // Target swapchain image + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; // Optimal for color rendering + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear for fresh scene start + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; // Preserve for UI overlay + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; // Clear to black + + // Configure depth attachment for 3D scene depth testing + // UI rendering won't need depth testing, so this is scene-specific vk::RenderingAttachmentInfo depthAttachment{}; - depthAttachment.imageView = *depthImageView; - depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; - depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; - depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; - depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + depthAttachment.imageView = *depthImageView; // Scene depth buffer + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; // Optimal for depth ops + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear depth for new frame + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; // UI doesn't need depth data + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; // Clear to far plane +---- + +The attachment configuration for scene rendering emphasizes the separation of concerns between 3D and UI rendering. The store operation for the color attachment ensures that scene rendering results are preserved for the subsequent UI overlay, while the depth attachment uses "don't care" storage since UI elements typically render without depth testing. + +This explicit configuration makes the rendering dependencies clear and helps optimize memory bandwidth by only preserving the data that subsequent passes actually need. + +=== Multi-Buffer: Scene Rendering Execution +The scene rendering execution occurs within its dedicated command buffer, providing isolated control over 3D rendering operations. + +[source,cpp] +---- + // Assemble scene rendering configuration + // This defines the complete 3D rendering context vk::RenderingInfo renderingInfo{}; - renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; - renderingInfo.layerCount = 1; - renderingInfo.colorAttachmentCount = 1; - renderingInfo.pColorAttachments = &colorAttachment; - renderingInfo.pDepthAttachment = &depthAttachment; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; // Full screen rendering + renderingInfo.layerCount = 1; // Single rendering layer + renderingInfo.colorAttachmentCount = 1; // One color output + renderingInfo.pColorAttachments = &colorAttachment; // Scene color configuration + renderingInfo.pDepthAttachment = &depthAttachment; // Scene depth configuration + // Execute complete 3D scene rendering pass sceneCommandBuffer.beginRendering(renderingInfo); - - // Render 3D scene + // All 3D geometry, lighting, materials, and effects render here // ... your existing scene rendering code ... - sceneCommandBuffer.endRendering(); + + // Finalize scene command buffer for submission sceneCommandBuffer.end(); +---- + +The scene rendering execution benefits from having its own isolated command buffer context, where all GPU state changes and draw calls are contained within a clearly defined scope. This isolation makes debugging easier, as scene-specific rendering issues can be analyzed independently of UI rendering complexity. - // Record ImGui command buffer +Command buffer finalization with `end()` transitions the buffer to an executable state, ready for GPU submission, while maintaining clear boundaries between different rendering responsibilities. + +=== Multi-Buffer: UI Command Buffer Setup + +The UI rendering phase begins with its own command buffer recording, configured specifically for overlay rendering requirements. + +[source,cpp] +---- + // Initialize UI-specific command buffer recording + // This dedicated buffer handles only UI overlay operations imguiCommandBuffer.begin(beginInfo); - // For ImGui, we want to preserve the contents of the previous rendering - colorAttachment.loadOp = vk::AttachmentLoadOp::eLoad; + // Configure UI attachment to preserve scene rendering results + // This is the key difference from scene rendering - we load existing content + colorAttachment.loadOp = vk::AttachmentLoadOp::eLoad; // Preserve scene rendering - // No need for depth attachment for UI + // UI rendering typically doesn't need depth testing + // Remove depth attachment to optimize UI rendering performance renderingInfo.pDepthAttachment = nullptr; +---- + +The UI command buffer setup demonstrates the power of the multi-buffer approach through its different attachment configuration. By changing the load operation to `eLoad`, we preserve the scene rendering results as the foundation for UI overlay rendering. This approach is more explicit and controllable than relying on automatic render pass dependencies. + +Removing the depth attachment for UI rendering eliminates unnecessary depth testing overhead, since UI elements typically render in screen space without complex occlusion relationships. This optimization can provide measurable performance improvements, especially on mobile GPUs where bandwidth is at a premium. - // Render ImGui using our custom renderer - // ImGui will handle its own dynamic rendering internally +=== Multi-Buffer: UI Rendering and Submission Coordination + +The final phase handles UI rendering execution and coordinates the submission of both command buffers in the correct order. + +[source,cpp] +---- + // Execute UI overlay rendering + // The custom renderer handles ImGui's dynamic rendering internally renderer.render(ImGui::GetDrawData(), imguiCommandBuffer); + // Finalize UI command buffer imguiCommandBuffer.end(); - // Submit command buffers in order + // Coordinate submission of both command buffers in dependency order + // Scene must complete before UI to ensure proper overlay rendering std::array submitCommandBuffers = { - *sceneCommandBuffer, - *imguiCommandBuffer + *sceneCommandBuffer, // Execute scene rendering first + *imguiCommandBuffer // Then execute UI overlay }; + // Configure batch submission for optimal GPU utilization vk::SubmitInfo submitInfo{}; submitInfo.commandBufferCount = static_cast(submitCommandBuffers.size()); submitInfo.pCommandBuffers = submitCommandBuffers.data(); + // Submit both command buffers as a cohesive frame // ... rest of your submission code ... } ---- diff --git a/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc index 23795eb3..798b1d0c 100644 --- a/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: Conclusion -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ == Conclusion diff --git a/en/Building_a_Simple_Engine/GUI/index.adoc b/en/Building_a_Simple_Engine/GUI/index.adoc index f0d5e4df..1578b7f9 100644 --- a/en/Building_a_Simple_Engine/GUI/index.adoc +++ b/en/Building_a_Simple_Engine/GUI/index.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = GUI: Introduce working with a GUI and handling input -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ include::01_introduction.adoc[] From 5e385406f622d39642ebde1de5463079f37b5284 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 15 Aug 2025 13:09:00 -0700 Subject: [PATCH 055/102] committing requested changes in stages. --- .../Lighting_Materials/01_introduction.adoc | 3 + .../Lighting_Materials/03_push_constants.adoc | 17 +- .../04_lighting_implementation.adoc | 665 +++++++++++++----- .../05_vulkan_integration.adoc | 266 ++++--- .../Lighting_Materials/06_conclusion.adoc | 49 +- .../Lighting_Materials/index.adoc | 9 +- images/bistro.png | Bin 0 -> 735878 bytes 7 files changed, 666 insertions(+), 343 deletions(-) create mode 100644 images/bistro.png diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc index 79ac7435..4b316387 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc @@ -2,6 +2,9 @@ In this chapter, we'll explore the fundamentals of lighting and materials in 3D rendering, with a focus on Physically Based Rendering (PBR). Lighting is a crucial aspect of creating realistic and visually appealing 3D scenes. Without proper lighting, even the most detailed models can appear flat and lifeless. +image:../../../images/bistro.png[Bistro scene with PBR, width=600, alt=Rendering the Bistro scene at night with PBR pass] + + [NOTE] ==== *About PBR References*: Throughout this tutorial, you may encounter references to PBR (Physically Based Rendering) before reaching this chapter. PBR is a modern rendering approach that simulates how light interacts with surfaces based on physical principles. We'll cover PBR in detail in the sections that follow, so don't worry if you're not familiar with these concepts yet. diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc index beeb3286..46957165 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc @@ -2,28 +2,15 @@ In this section, we'll explore push constants, a powerful feature in Vulkan that allows us to efficiently pass small amounts of data to shaders without the overhead of descriptor sets. -Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. - == What Are Push Constants? Push constants are a way to send a small amount of data directly to shaders. Unlike uniform buffers, which require descriptor sets and memory allocation, push constants are part of the command buffer itself. This makes them ideal for small, frequently changing data. -Some key characteristics of push constants: - -1. *Size Limitations*: Push constants have a limited size (typically 128 bytes, but this can vary depending on the device). -2. *Efficiency*: They're more efficient than uniform buffers for small, frequently changing data. -3. *Simplicity*: They don't require descriptor sets or memory allocation. -4. *Scope*: They're available to all shader stages in a pipeline. +Some key characteristics of push constants: they are tiny (typically up to 128 bytes, device dependent), fast to update per draw, and require no descriptor sets or allocations because they live on the command buffer. They can be read by any shader stage you enable in the pipeline. == When to Use Push Constants -Push constants are particularly useful for: - -1. *Per-Draw Data*: Data that changes for each draw call, such as material properties. -2. *Small Data Sets*: Data that fits within the size limitations of push constants. -3. *Frequently Changing Data*: Data that changes often, where the overhead of updating a uniform buffer would be significant. - -For our PBR implementation, push constants are perfect for storing material properties like base color, metallic factor, and roughness factor. +Use push constants for tiny, per‑draw parameters that change frequently—exactly the kind of material knobs (base color, metallic, roughness) we tweak per object. If the data is larger than the device’s push‑constant limit or doesn’t change often, prefer a uniform buffer instead. == Defining Push Constants in Shaders diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc index 8a258858..723a3c60 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc @@ -2,65 +2,69 @@ In this section, we'll implement a Physically Based Rendering (PBR) shader based on the concepts we've explored in the previous sections. This shader will use the metallic-roughness workflow that's compatible with glTF models and push constants for material properties. We'll examine the shader implementation and then discuss how to integrate it with our engine. -Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. - == Implementing the PBR Shader Let's create a PBR shader, which we'll name `pbr.slang`. This shader implements the metallic-roughness workflow that we've discussed, making it compatible with glTF models. It uses push constants for material properties and uniform buffers for transformation matrices and light information. +We'll break this shader into three distinct sections to better understand its architecture: + +=== Section 1: Shader Setup Code - CPU-GPU Communication + +This section establishes the communication interface between the CPU application and GPU shader. It defines the data structures and bindings that allow the CPU to pass information to the GPU efficiently. + [source,slang] ---- // Combined vertex and fragment shader for PBR rendering -// Input from vertex buffer +// Input from vertex buffer - Data sent per vertex from CPU struct VSInput { - float3 Position : POSITION; - float3 Normal : NORMAL; - float2 UV : TEXCOORD0; - float4 Tangent : TANGENT; + float3 Position : POSITION; // 3D position in model space + float3 Normal : NORMAL; // Surface normal for lighting calculations + float2 UV : TEXCOORD0; // Texture coordinates for material sampling + float4 Tangent : TANGENT; // Tangent vector for normal mapping (w component = handedness) }; -// Output from vertex shader / Input to fragment shader +// Output from vertex shader / Input to fragment shader - Interpolated data struct VSOutput { - float4 Position : SV_POSITION; - float3 WorldPos : POSITION; - float3 Normal : NORMAL; - float2 UV : TEXCOORD0; - float4 Tangent : TANGENT; + float4 Position : SV_POSITION; // Required clip space position for rasterization + float3 WorldPos : POSITION; // World space position for lighting calculations + float3 Normal : NORMAL; // World space normal (interpolated) + float2 UV : TEXCOORD0; // Texture coordinates (interpolated) + float4 Tangent : TANGENT; // World space tangent (interpolated) }; -// Uniform buffer +// Uniform buffer - Global data shared across all vertices/fragments struct UniformBufferObject { - float4x4 model; - float4x4 view; - float4x4 proj; - float4 lightPositions[4]; - float4 lightColors[4]; - float4 camPos; - float exposure; - float gamma; - float prefilteredCubeMipLevels; - float scaleIBLAmbient; + float4x4 model; // Model-to-world transformation matrix + float4x4 view; // World-to-camera transformation matrix + float4x4 proj; // Camera-to-clip space projection matrix + float4 lightPositions[4]; // Light positions in world space + float4 lightColors[4]; // Light intensities and colors + float4 camPos; // Camera position for view-dependent effects + float exposure; // HDR exposure control + float gamma; // Gamma correction value (typically 2.2) + float prefilteredCubeMipLevels; // IBL prefiltered environment map mip levels + float scaleIBLAmbient; // IBL ambient contribution scale }; -// Push constants for material properties +// Push constants - Fast, small data updated frequently per material/object struct PushConstants { - float4 baseColorFactor; - float metallicFactor; - float roughnessFactor; - int baseColorTextureSet; - int physicalDescriptorTextureSet; - int normalTextureSet; - int occlusionTextureSet; - int emissiveTextureSet; - float alphaMask; - float alphaMaskCutoff; + float4 baseColorFactor; // Base color tint/multiplier + float metallicFactor; // Metallic property multiplier + float roughnessFactor; // Surface roughness multiplier + int baseColorTextureSet; // Texture binding index for base color (-1 = none) + int physicalDescriptorTextureSet; // Texture binding for metallic/roughness + int normalTextureSet; // Texture binding for normal maps + int occlusionTextureSet; // Texture binding for ambient occlusion + int emissiveTextureSet; // Texture binding for emissive maps + float alphaMask; // Alpha masking enable flag + float alphaMaskCutoff; // Alpha cutoff threshold }; -// Constants +// Mathematical constants static const float PI = 3.14159265359; -// Bindings +// Resource bindings - Connect CPU resources to GPU shader registers [[vk::binding(0, 0)]] ConstantBuffer ubo; [[vk::binding(1, 0)]] Texture2D baseColorMap; [[vk::binding(1, 0)]] SamplerState baseColorSampler; @@ -74,152 +78,301 @@ static const float PI = 3.14159265359; [[vk::binding(5, 0)]] SamplerState emissiveSampler; [[vk::push_constant]] PushConstants material; +---- + +**Key Concepts Explained:** + +The vertex input layout defines how vertex data is structured in GPU memory, with semantic annotations like POSITION and NORMAL telling the GPU how to interpret each data component. This structured approach allows the graphics pipeline to efficiently process vertex attributes and pass them through the rendering stages. + +When it comes to data management, we use two primary mechanisms: uniform buffers and push constants. Uniform buffers are larger, read-only memory blocks that efficiently store data shared across many draw calls, making them perfect for transformation matrices and lighting information that remain constant across multiple objects. Push constants, on the other hand, are smaller (typically limited to 128 bytes or less) but much faster for frequently changing per-object data like material properties, making them ideal for our material system. + +The resource binding syntax using `pass:[[[vk::binding(x, y)]]]` creates the essential link between CPU resources and GPU shader registers. The first number represents the binding index, while the second specifies the descriptor set, allowing us to organize and efficiently access textures, samplers, and other resources from within our shaders. -// PBR functions +Finally, the interpolation system works seamlessly in the background, where data in our `VSOutput` structure gets automatically interpolated across triangle surfaces by the GPU's rasterization hardware, ensuring smooth transitions of attributes like normals and texture coordinates across the rendered surface. + +=== Section 2: Helper Functions - PBR Mathematics + +This section contains the mathematical foundation of Physically Based Rendering. These functions implement the Cook-Torrance microfacet BRDF model, which approximates how light interacts with real-world materials at a microscopic level. + +[source,slang] +---- +// Normal Distribution Function (D) - GGX/Trowbridge-Reitz Distribution +// Describes the statistical distribution of microfacet orientations float DistributionGGX(float NdotH, float roughness) { - float a = roughness * roughness; + float a = roughness * roughness; // Remapping for more perceptual linearity float a2 = a * a; float NdotH2 = NdotH * NdotH; - float nom = a2; + float nom = a2; // Numerator: concentration factor float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; + denom = PI * denom * denom; // Normalization factor - return nom / denom; + return nom / denom; // Normalized distribution } +// Geometry Function (G) - Smith's method with Schlick-GGX approximation +// Models self-shadowing and masking between microfacets float GeometrySmith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; - float k = (r * r) / 8.0; + float k = (r * r) / 8.0; // Direct lighting remapping + // Geometry obstruction from view direction (masking) float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + // Geometry obstruction from light direction (shadowing) float ggx2 = NdotL / (NdotL * (1.0 - k) + k); - return ggx1 * ggx2; + return ggx1 * ggx2; // Combined masking-shadowing } +// Fresnel Reflectance (F) - Schlick's approximation +// Models how reflectance changes with viewing angle float3 FresnelSchlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } +---- + +**Mathematical Concepts & References:** + +The foundation of our PBR implementation rests on microfacet theory, which recognizes that real surfaces consist of countless microscopic facets with varying orientations. Rather than trying to model each individual facet, the BRDF statistically represents their collective behavior, allowing us to achieve realistic lighting without the computational complexity of simulating every surface detail. This approach was thoroughly explored in Walter et al.'s seminal 2007 paper "Microfacet Models for Refraction through Rough Surfaces," which you can find at link:https://www.graphics.cornell.edu/~bjw/microfacetbsdf.pdf[their comprehensive BSDF documentation]. + +Our choice of the GGX distribution function, also known as Trowbridge-Reitz, stems from its ability to produce realistic highlight shapes with longer tails compared to older models like Blinn-Phong. This distribution function has become the standard in modern real-time rendering because it closely matches measured material data and provides the natural falloff that we observe in real-world materials. Eric Heitz's 2014 work "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs" provides deep insights into why this distribution works so well in practice. -// Vertex shader entry point +The Smith geometry function plays a crucial role by accounting for the statistical correlation between masking (when the viewer can't see a microfacet) and shadowing (when light can't reach a microfacet). This might seem like a technical detail, but it prevents energy gain at grazing angles where naive models become unrealistically bright, ensuring our materials look believable under all viewing conditions. + +The Fresnel effect captures a phenomenon we see every day: materials become more reflective at grazing angles, like water appearing mirror-like when viewed from the side. Schlick's approximation gives us this essential behavior while trading some accuracy for the performance we need in real-time applications. The F0 parameter represents reflectance at normal incidence (0° viewing angle), allowing us to control how reflective different materials appear when viewed head-on. + +Finally, energy conservation ensures that the sum of reflected and transmitted light never exceeds the incident light, maintaining physical plausibility. This principle guides how we balance diffuse and specular components, ensuring our materials look consistent and believable under varying lighting conditions. + +**Further Reading:** + +For deeper exploration of these concepts, "Real-Time Rendering, 4th Edition" Chapter 9 on Physically Based Shading provides comprehensive coverage of the theory and practice. The online "PBR Book" by Pharr, Jakob, and Humphreys at https://pbr-book.org/ offers an exhaustive mathematical treatment of physically based rendering. For practical implementation insights, Epic Games' "Real Shading in Unreal Engine 4" presentation from the link:https://blog.selfshadow.com/publications/s2013-shading-course/[2013 Shading Course] demonstrates how these concepts translate into production-ready code. + +=== Section 3: Vertex and Fragment Shader Main Bodies + +This section contains the actual shader entry points that execute for each vertex and fragment (pixel). The vertex shader transforms geometry, while the fragment shader implements the full PBR lighting model. + +[source,slang] +---- +// Vertex shader entry point - Executes once per vertex [[shader("vertex")]] VSOutput VSMain(VSInput input) { VSOutput output; - // Transform position to clip space + // Transform vertex position through the rendering pipeline + // Model -> World -> Camera -> Clip space transformation chain float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); - // Pass world position to fragment shader + // Pass world position for fragment lighting calculations + // Fragment shader needs world space position to calculate light vectors output.WorldPos = worldPos.xyz; - // Transform normal to world space + // Transform normal from model space to world space + // Use only rotation/scale part of model matrix (upper-left 3x3) + // Normalize to ensure unit length after transformation output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); - // Pass texture coordinates + // Pass through texture coordinates unchanged + // UV coordinates are typically in [0,1] range and don't need transformation output.UV = input.UV; - // Pass tangent + // Pass tangent vector for normal mapping + // Will be used in fragment shader to construct tangent-space basis output.Tangent = input.Tangent; return output; } -// Fragment shader entry point +// Fragment shader entry point - Executes once per pixel [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET { - // Sample material textures + // === MATERIAL PROPERTY SAMPLING === + // Sample base color texture and apply material color factor float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + + // Sample metallic-roughness texture (metallic=B channel, roughness=G channel) + // glTF standard: metallic stored in blue, roughness in green float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; float metallic = metallicRoughness.x * material.metallicFactor; float roughness = metallicRoughness.y * material.roughnessFactor; + + // Sample ambient occlusion (typically stored in red channel) float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; + + // Sample emissive texture for self-illuminating materials float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; - // Calculate normal in tangent space + // === NORMAL CALCULATION === + // Start with interpolated surface normal float3 N = normalize(input.Normal); + + // Apply normal mapping if texture is available if (material.normalTextureSet >= 0) { - // Apply normal mapping + // Sample normal map and convert from [0,1] to [-1,1] range float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; - float3 T = normalize(input.Tangent.xyz); - float3 B = normalize(cross(N, T)) * input.Tangent.w; - float3x3 TBN = float3x3(T, B, N); + + // Construct tangent-space to world-space transformation matrix (TBN) + float3 T = normalize(input.Tangent.xyz); // Tangent + float3 B = normalize(cross(N, T)) * input.Tangent.w; // Bitangent (w = handedness) + float3x3 TBN = float3x3(T, B, N); // Tangent-Bitangent-Normal matrix + + // Transform normal from tangent space to world space N = normalize(mul(tangentNormal, TBN)); } - // Calculate view and reflection vectors + // === LIGHTING SETUP === + // Calculate view direction (camera to fragment) float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + + // Calculate reflection vector for environment mapping float3 R = reflect(-V, N); - // Calculate F0 (base reflectivity) - float3 F0 = float3(0.04, 0.04, 0.04); - F0 = lerp(F0, baseColor.rgb, metallic); + // === PBR MATERIAL SETUP === + // Calculate F0 (reflectance at normal incidence) + // Non-metals: low reflectance (~0.04), Metals: colored reflectance from base color + float3 F0 = float3(0.04, 0.04, 0.04); // Dielectric default + F0 = lerp(F0, baseColor.rgb, metallic); // Lerp to metallic behavior - // Initialize lighting + // Initialize outgoing radiance accumulator float3 Lo = float3(0.0, 0.0, 0.0); - // Calculate lighting for each light + // === DIRECT LIGHTING LOOP === + // Calculate contribution from each light source for (int i = 0; i < 4; i++) { float3 lightPos = ubo.lightPositions[i].xyz; float3 lightColor = ubo.lightColors[i].rgb; - // Calculate light direction and distance - float3 L = normalize(lightPos - input.WorldPos); - float distance = length(lightPos - input.WorldPos); - float attenuation = 1.0 / (distance * distance); - float3 radiance = lightColor * attenuation; + // Calculate light direction and attenuation + float3 L = normalize(lightPos - input.WorldPos); // Light direction + float distance = length(lightPos - input.WorldPos); // Distance for falloff + float attenuation = 1.0 / (distance * distance); // Inverse square falloff + float3 radiance = lightColor * attenuation; // Attenuated light color - // Calculate half vector + // Calculate half vector (between view and light directions) float3 H = normalize(V + L); - // Calculate BRDF terms - float NdotL = max(dot(N, L), 0.0); - float NdotV = max(dot(N, V), 0.0); - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); + // === BRDF EVALUATION === + // Calculate all necessary dot products for BRDF terms + float NdotL = max(dot(N, L), 0.0); // Lambertian falloff + float NdotV = max(dot(N, V), 0.0); // View angle + float NdotH = max(dot(N, H), 0.0); // Half vector for specular + float HdotV = max(dot(H, V), 0.0); // For Fresnel calculation - // Specular BRDF - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmith(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(HdotV, F0); + // Evaluate Cook-Torrance BRDF components + float D = DistributionGGX(NdotH, roughness); // Normal distribution + float G = GeometrySmith(NdotV, NdotL, roughness); // Geometry function + float3 F = FresnelSchlick(HdotV, F0); // Fresnel reflectance + // Calculate specular BRDF float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; + float denominator = 4.0 * NdotV * NdotL + 0.0001; // Prevent division by zero float3 specular = numerator / denominator; - // Energy conservation - float3 kS = F; - float3 kD = float3(1.0, 1.0, 1.0) - kS; - kD *= 1.0 - metallic; + // === ENERGY CONSERVATION === + // Fresnel term represents specular reflection ratio + float3 kS = F; // Specular contribution + float3 kD = float3(1.0, 1.0, 1.0) - kS; // Diffuse contribution (energy conservation) + kD *= 1.0 - metallic; // Metals have no diffuse reflection - // Add to outgoing radiance + // === RADIANCE ACCUMULATION === + // Combine diffuse (Lambertian) and specular (Cook-Torrance) terms + // Multiply by incident radiance and cosine foreshortening Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; } - // Add ambient and emissive + // === AMBIENT AND EMISSIVE === + // Add simple ambient lighting (should be replaced with IBL in production) float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + + // Combine all lighting contributions float3 color = ambient + Lo + emissive; - // HDR tonemapping and gamma correction + // === HDR TONE MAPPING AND GAMMA CORRECTION === + // Apply Reinhard tone mapping to compress HDR values to [0,1] range color = color / (color + float3(1.0, 1.0, 1.0)); + + // Apply gamma correction for sRGB display (inverse gamma) color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + // Output final color with original alpha return float4(color, baseColor.a); } ---- -This shader implements the PBR lighting model with the metallic-roughness workflow. It includes: +**Vertex Shader Objectives:** + +The vertex shader serves as the first stage of our rendering pipeline, with its primary responsibility being geometric transformation. It converts vertex positions through the standard MVP (Model-View-Projection) matrix pipeline, systematically transforming coordinates from model space to world space, then to camera space, and finally to clip space in preparation for rasterization. This transformation chain ensures that our 3D geometry appears correctly positioned and projected for the viewer. + +Beyond basic transformation, the vertex shader handles crucial attribute processing by transforming normals from model space to world space and passing through texture coordinates and tangent vectors that the fragment shader will need. This attribute processing ensures that lighting calculations in the fragment shader receive properly transformed surface information, while texture coordinates and tangent vectors maintain their relationships for accurate material sampling and normal mapping. + +The vertex shader also performs essential data preparation by setting up interpolated values that the fragment shader requires for lighting calculations. These interpolated values, such as world positions and transformed normals, get automatically interpolated across triangle surfaces by the GPU's rasterization hardware, providing smooth transitions that enable realistic per-pixel lighting in the subsequent fragment stage. + +**Fragment Shader Objectives:** + +The fragment shader represents the heart of our PBR implementation, beginning with comprehensive material sampling that extracts surface properties like color, roughness, and metallic values from texture maps. This sampling process reads multiple texture channels according to the glTF standard, combining texture data with material parameters passed through push constants to determine the final surface characteristics for each pixel. + +Normal mapping reconstruction forms another critical objective, where the fragment shader takes encoded normal information from normal maps and reconstructs detailed surface normals that simulate fine geometric detail without requiring additional geometry. This process involves sampling the normal map, transforming the values from texture space to world space using the tangent-bitangent-normal matrix, and applying the resulting detailed normals to lighting calculations. + +The core PBR lighting implementation brings together all these elements using the Cook-Torrance microfacet model with proper energy conservation. This involves evaluating the distribution, geometry, and Fresnel terms of the BRDF, carefully balancing diffuse and specular contributions to ensure physically plausible results across all viewing angles and material types. + +Finally, post-processing operations convert the HDR linear lighting results into display-appropriate sRGB values through tone mapping and gamma correction. This final stage compresses the high dynamic range values generated by realistic lighting calculations into the limited range that displays can show, while maintaining visual fidelity and preventing the harsh clipping that would otherwise occur with bright highlights. + +**Key Implementation Details:** + +Our implementation carefully follows established conventions and best practices to ensure compatibility and visual quality. We adhere to the glTF texture channel convention where metallic information uses the blue channel and roughness uses the green channel, enabling seamless integration with standard 3D authoring tools and asset pipelines. This convention ensures that materials created in external tools will render correctly without requiring texture channel remapping or custom import procedures. + +Energy conservation remains paramount throughout our implementation, with careful attention paid to ensuring that diffuse plus specular contributions never exceed unity through the kS/kD relationship. This physical constraint prevents materials from appearing to emit more light than they receive, maintaining believable appearance across different lighting conditions and viewing angles while avoiding the artificial brightness that can plague non-physically-based approaches. + +Numerical stability considerations appear throughout the implementation, with small epsilon values added to prevent division by zero in BRDF calculations and careful handling of edge cases where mathematical operations might produce undefined results. These seemingly minor details prove crucial for robust rendering that handles extreme material parameters and unusual viewing angles without producing artifacts or rendering failures. + +The HDR pipeline architecture ensures that all lighting calculations occur in linear space, preserving the full dynamic range of realistic lighting throughout the computation stages and only applying gamma correction at the final output stage. This approach maintains maximum precision and accuracy in the lighting calculations while ensuring that the final image appears correct on standard sRGB displays. + +This shader implements the PBR lighting model with the metallic-roughness workflow, but the goal here is not just to show "what" the code does — it's to explain "why" each piece exists. + +== Understanding the "Why" behind the shader + +=== Why these BRDF terms (D, G, F) + +The Normal Distribution Function (D) serves as the statistical heart of our microfacet model, determining how many surface microfacets are oriented to reflect light directly toward the viewer. This function explains why rough surfaces produce broader, dimmer highlights while smooth surfaces create tight, bright reflections. We chose the GGX distribution because it matches measured material data remarkably well and produces the natural long tails in highlights that we observe in real-world materials, avoiding the artificial cutoff that plagued older distribution functions like Blinn-Phong. -1. *Normal Distribution Function (D)*: Using the GGX (Trowbridge-Reitz) distribution -2. *Geometry Function (G)*: Using the Smith shadowing-masking function -3. *Fresnel Term (F)*: Using Schlick's approximation -4. *Energy Conservation*: Ensuring that diffuse and specular reflection don't exceed the incoming light -5. *Normal Mapping*: For adding surface detail without increasing geometric complexity -6. *HDR Tonemapping*: For handling high dynamic range lighting -7. *Gamma Correction*: For proper color representation +The Geometry function (G) addresses a crucial physical reality: microfacets cast shadows on each other and can be hidden from view depending on the surface roughness and viewing angle. Without proper geometric consideration, highlights become unrealistically bright as roughness increases because we'd be ignoring the natural self-shadowing and masking that occurs on rough surfaces. Smith's approach with our roughness-derived k parameter provides an efficient yet physically plausible solution that maintains energy conservation across all viewing conditions. + +Fresnel reflectance (F) captures one of the most fundamental optical phenomena we encounter daily: surfaces become more reflective at grazing angles, just as you can see your reflection clearly in water when looking across its surface but hardly at all when looking straight down. Schlick's approximation gives us this essential angle-dependent behavior with minimal computational cost, while the F0 parameter allows us to control how reflective materials appear when viewed head-on, distinguishing between different material types. + +Energy conservation ties these components together by ensuring that the sum of reflected light never exceeds the incident light, maintaining physical plausibility. When more light reflects specularly (kS), correspondingly less can reflect diffusely (kD = 1 - kS), creating the natural balance that keeps materials looking believable across different lighting conditions and viewing angles while preventing the artificial brightness that can make rendered scenes look unrealistic. + +=== Why the metallic-roughness + +The metallic-roughness workflow has become the industry standard primarily due to its adoption by the glTF specification, which standardizes this approach with metallic information stored in the blue channel and roughness in the green channel by convention. This standardization creates a seamless ecosystem where assets created in any glTF-compliant tool will render consistently across different engines and applications, eliminating the texture channel confusion that plagued earlier workflows and enabling true asset interoperability. + +From an artistic perspective, this workflow proves remarkably intuitive because it presents artists with just two conceptual dials to control: metalness (distinguishing between non-metals and metals) and roughness (controlling the surface finish from perfectly smooth to completely rough), plus the base color. This simplification allows artists to focus on the visual intent rather than getting lost in complex parameter interactions, while still providing the full range of material appearances found in the real world. + +The workflow also handles F0 behavior correctly by encoding the fundamental difference between metallic and non-metallic materials. Non-metals typically have low F0 values around 0.02 to 0.08 (we use 0.04 as a reasonable default), while metals derive their colored specular reflectance directly from the base color. Our lerp(F0, baseColor, metallic) operation elegantly encodes this physical distinction, automatically transitioning from the achromatic reflectance of dielectrics to the colored reflectance of conductors as the metallic parameter increases. + +=== Why normal, occlusion, and emissive maps +Normal mapping represents one of the most powerful techniques in modern real-time rendering, allowing us to add high-frequency surface detail without increasing geometric complexity. By storing surface perturbations as RGB values in a texture, we can simulate fine details like scratches, rivets, or fabric weaves that would be prohibitively expensive to model with actual geometry. The magic happens in tangent space, where we reconstruct the perturbed normal vector N from the tangent-bitangent-normal (TBN) matrix, ensuring that lighting calculations respond to these small-scale surface features as if they were real geometric details. +- Ambient occlusion (AO): Dampens indirect light in crevices the global model doesn’t capture. We multiply the ambient/IBL term by AO to avoid overly flat shading. +- Emissive: Lets materials glow independent of lighting (e.g., LEDs, screens) and contributes additively so it’s visible even in darkness. + +=== Why HDR, exposure, and tone mapping +- Realistic light intensities create values far beyond [0,1] (e.g., sunlit surfaces, bright emitters). If we write those directly to an 8-bit display, they clip at 1.0, crushing detail and producing ugly, step-like highlights. +- Working in HDR (linear float) preserves detail through the lighting pipeline. Only at the end do we compress dynamic range using a tone mapper to fit the display. +- In this chapter we use simple Reinhard: color / (color + 1). It’s robust and artifact-free, good as a baseline. Alternatives you might adopt later: + * ACES (RRT/ODT): Filmic with good color preservation across extremes; widely used. + * Hable/Uncharted2 (“Filmic”): Nice highlight roll-off, tunable via curve parameters. + * Reinhard with exposure: Multiply color by an exposure before compressing to shift middle gray. +- Exposure parameter (ubo.exposure): Conceptually shifts scene brightness so midtones sit well under your chosen tone mapper. Even if the snippet shows a fixed operator, you can pre-scale color by exposure to support dynamic auto-exposure. +- Gamma correction (ubo.gamma): Displays are non-linear (approx 2.2). Lighting must happen in linear space, then we apply pow(color, 1/gamma) right before writing to the sRGB framebuffer. Skipping this causes washed-out or too-dark images. +- Pipeline note: Prefer sRGB formats for color attachments when presenting. If writing to an sRGB swapchain image, do gamma in shader OR use sRGB formats so hardware handles it — not both. Do exactly one. + +=== Practical tuning checklist +- If highlights look “plasticky” everywhere, roughness may be too low or kD not reduced by metallic; verify kD *= (1 - metallic). +- If everything clips to white, add/adjust exposure and switch to ACES or Filmic tone mapping. +- If colors shift in highlights, check that tone mapping happens in linear space and gamma is applied only once. +- If normal maps look inverted or seams appear, verify tangent handedness (TBN), normal map channel order, and normal map space. +- If ambient looks flat, confirm AO is applied to ambient/IBL but not to direct specular. == Extending the Renderer @@ -229,178 +382,296 @@ Now that we have our PBR shader, we need to extend our renderer to support it. W 2. Add support for push constants 3. Update the uniform buffer to include light information -Let's start by adding a new function to create the PBR pipeline: +Let's start by adding a new function to create the PBR pipeline. This process involves several distinct steps, each serving a specific purpose in configuring the Vulkan graphics pipeline for physically based rendering. + +=== Shader Module Creation and Stage Setup + +First, we load our compiled shader and set up the programmable stages of the graphics pipeline. Vulkan requires us to explicitly specify which shader stages we'll use and their entry points. [source,cpp] ---- bool Renderer::createPBRPipeline() { try { - // Load combined PBR shader + // Load our compiled PBR shader from disk + // The .spv file contains both vertex and fragment shader code compiled by slangc auto shaderCode = readFile("shaders/pbr.spv"); - // Create shader module with vk::raii + // Create a shader module - this is Vulkan's container for shader bytecode + // The shader module acts as a wrapper around the SPIR-V bytecode that GPU drivers understand vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); - // Set up shader stage info + // Configure the vertex shader stage + // This tells Vulkan which shader stage this module serves and its entry point function vk::PipelineShaderStageCreateInfo vertShaderStageInfo; vertShaderStageInfo.setStage(vk::ShaderStageFlagBits::eVertex) .setModule(*shaderModule) - .setPName("VSMain"); // Entry point for vertex shader + .setPName("VSMain"); // Must match the vertex shader function name + // Configure the fragment shader stage + // Same module, different entry point - this is how combined shaders work vk::PipelineShaderStageCreateInfo fragShaderStageInfo; fragShaderStageInfo.setStage(vk::ShaderStageFlagBits::eFragment) .setModule(*shaderModule) - .setPName("PSMain"); // Entry point for fragment shader + .setPName("PSMain"); // Must match the fragment shader function name std::array shaderStages = {vertShaderStageInfo, fragShaderStageInfo}; +---- + +The entry point names ("VSMain" and "PSMain") must exactly match the function names in our shader code. This explicit binding system gives us fine-grained control over which functions serve which pipeline stages, and it's particularly useful when working with shader libraries that contain multiple variations of vertex or fragment shaders. - // Vertex input state +=== Vertex Input Configuration + +The vertex input state defines how vertex data flows from our vertex buffers into the vertex shader. This configuration must precisely match the vertex format expected by our PBR shader. + +[source,cpp] +---- + // Configure how vertex data is structured and fed to the vertex shader vk::PipelineVertexInputStateCreateInfo vertexInputInfo; - // Define vertex binding and attributes for PBR + // Define the vertex buffer binding - describes the overall vertex structure + // This tells Vulkan the total size of each vertex and how vertices are arranged vk::VertexInputBindingDescription bindingDescription; - bindingDescription.setBinding(0) - .setStride(sizeof(float) * 14) // pos(3) + normal(3) + texCoord(2) + tangent(4) + bitangent(2) - .setInputRate(vk::VertexInputRate::eVertex); + bindingDescription.setBinding(0) // Binding point 0 + .setStride(sizeof(float) * 14) // Total vertex size: pos(3) + normal(3) + uv(2) + tangent(4) + bitangent(2) + .setInputRate(vk::VertexInputRate::eVertex); // Data advances per vertex (not per instance) + // Define individual vertex attributes - each corresponds to an input in our vertex shader std::array attributeDescriptions; - // Position - attributeDescriptions[0].setBinding(0) - .setLocation(0) - .setFormat(vk::Format::eR32G32B32Sfloat) - .setOffset(0); - // Normal + + // Position attribute: 3D coordinates in model space + attributeDescriptions[0].setBinding(0) // From binding 0 + .setLocation(0) // Shader input location 0 + .setFormat(vk::Format::eR32G32B32Sfloat) // Three 32-bit floats (RGB) + .setOffset(0); // Start of vertex data + + // Normal attribute: surface normal for lighting calculations attributeDescriptions[1].setBinding(0) - .setLocation(1) + .setLocation(1) // Shader input location 1 .setFormat(vk::Format::eR32G32B32Sfloat) - .setOffset(sizeof(float) * 3); - // Texture coordinates + .setOffset(sizeof(float) * 3); // After position + + // Texture coordinate attribute: UV mapping coordinates attributeDescriptions[2].setBinding(0) - .setLocation(2) - .setFormat(vk::Format::eR32G32Sfloat) - .setOffset(sizeof(float) * 6); - // Tangent + .setLocation(2) // Shader input location 2 + .setFormat(vk::Format::eR32G32Sfloat) // Two 32-bit floats (RG) + .setOffset(sizeof(float) * 6); // After position + normal + + // Tangent attribute: tangent vector for normal mapping (includes handedness in W) attributeDescriptions[3].setBinding(0) - .setLocation(3) - .setFormat(vk::Format::eR32G32B32A32Sfloat) - .setOffset(sizeof(float) * 8); - // Bitangent + .setLocation(3) // Shader input location 3 + .setFormat(vk::Format::eR32G32B32A32Sfloat) // Four 32-bit floats (RGBA) + .setOffset(sizeof(float) * 8); // After position + normal + UV + + // Bitangent attribute: completes the tangent space basis attributeDescriptions[4].setBinding(0) - .setLocation(4) + .setLocation(4) // Shader input location 4 .setFormat(vk::Format::eR32G32Sfloat) - .setOffset(sizeof(float) * 12); + .setOffset(sizeof(float) * 12); // After all previous attributes + // Connect the binding and attribute descriptions to the vertex input state vertexInputInfo.setVertexBindingDescriptionCount(1) .setPVertexBindingDescriptions(&bindingDescription) .setVertexAttributeDescriptionCount(static_cast(attributeDescriptions.size())) .setPVertexAttributeDescriptions(attributeDescriptions.data()); +---- + +The vertex input configuration serves as a contract between our vertex buffer data and the vertex shader inputs. Each attribute description maps a specific piece of vertex data to a shader input location, with precise format and offset specifications. This explicit mapping system ensures that the GPU correctly interprets our vertex data regardless of how it's packed in memory. - // Input assembly state +The stride calculation (14 floats) reflects our comprehensive vertex format that supports full PBR rendering: position for geometry, normals for basic lighting, UV coordinates for texture sampling, and tangent vectors for normal mapping. The tangent vector includes a fourth component (W) that stores handedness information, which is crucial for correctly reconstructing the bitangent vector in cases where the tangent space might be flipped. + +The offset calculations ensure that each attribute starts at the correct byte position within each vertex. This precise alignment is for performance, as misaligned vertex data can cause significant performance penalties on some GPU architectures. + +=== Input Assembly and Primitive Processing + +The input assembly stage determines how vertices are grouped into geometric primitives and how the GPU should interpret the vertex stream. + +[source,cpp] +---- + // Configure input assembly - how vertices become triangles vk::PipelineInputAssemblyStateCreateInfo inputAssembly; - inputAssembly.setTopology(vk::PrimitiveTopology::eTriangleList) - .setPrimitiveRestartEnable(false); + inputAssembly.setTopology(vk::PrimitiveTopology::eTriangleList) // Every 3 vertices form a triangle + .setPrimitiveRestartEnable(false); // Don't use primitive restart indices +---- + +Triangle lists represent the most straightforward and commonly used primitive topology for complex 3D models. In this mode, every group of three consecutive vertices defines a complete triangle, providing maximum flexibility for representing arbitrary geometry. While other topologies like triangle strips or fans can be more memory-efficient for certain geometric patterns, triangle lists avoid the complexity of degenerate triangles and vertex ordering constraints that can arise with more compact representations. + +Primitive restart functionality allows special index values to signal the end of one primitive and the beginning of another, but this feature adds complexity that's unnecessary for most PBR rendering scenarios. By disabling it, we ensure predictable behavior and avoid potential performance penalties associated with index buffer scanning. - // Viewport and scissor state +=== Viewport and Dynamic State Configuration + +The viewport state manages the transformation from normalized device coordinates to screen coordinates, while dynamic state configuration allows certain pipeline parameters to be changed without recreating the entire pipeline. + +[source,cpp] +---- + // Configure viewport and scissor state + // We'll set actual viewport and scissor rectangles dynamically at render time vk::PipelineViewportStateCreateInfo viewportState; - viewportState.setViewportCount(1) - .setScissorCount(1); + viewportState.setViewportCount(1) // Single viewport (most common case) + .setScissorCount(1); // Single scissor rectangle - // Dynamic state for viewport and scissor + // Define which pipeline state can be changed dynamically + // This improves performance by avoiding pipeline recreation for common changes std::vector dynamicStates = { - vk::DynamicState::eViewport, - vk::DynamicState::eScissor + vk::DynamicState::eViewport, // Viewport can change (window resize, camera changes) + vk::DynamicState::eScissor // Scissor rectangle can change (UI clipping, effects) }; vk::PipelineDynamicStateCreateInfo dynamicState; dynamicState.setDynamicStateCount(static_cast(dynamicStates.size())) .setPDynamicStates(dynamicStates.data()); +---- + +Dynamic state configuration represents a key optimization in modern Vulkan applications. By marking viewport and scissor as dynamic, we avoid the expensive pipeline recreation that would otherwise be required for common operations like window resizing or camera adjustments. The GPU driver can efficiently update these parameters at command recording time rather than requiring a completely new pipeline state object. + +The single viewport approach covers the vast majority of rendering scenarios. Multi-viewport rendering is primarily used for specialized applications like VR stereo rendering or certain shadow mapping techniques, but single-viewport rendering provides optimal performance for standard PBR applications. + +=== Rasterization Configuration + +The rasterization stage converts geometric primitives into fragments (potential pixels) and applies various geometric processing options that affect how triangles are converted to pixels. - // Rasterization state +[source,cpp] +---- + // Configure rasterization - how triangles become pixels vk::PipelineRasterizationStateCreateInfo rasterizer; - rasterizer.setDepthClampEnable(false) - .setRasterizerDiscardEnable(false) - .setPolygonMode(vk::PolygonMode::eFill) - .setLineWidth(1.0f) - .setCullMode(vk::CullModeFlagBits::eBack) - .setFrontFace(vk::FrontFace::eCounterClockwise) - .setDepthBiasEnable(false); - - // Multisample state + rasterizer.setDepthClampEnable(false) // Don't clamp depth values (standard behavior) + .setRasterizerDiscardEnable(false) // Don't discard primitives before rasterization + .setPolygonMode(vk::PolygonMode::eFill) // Fill triangles (not wireframe or points) + .setLineWidth(1.0f) // Line width (only relevant for wireframe) + .setCullMode(vk::CullModeFlagBits::eBack) // Cull back-facing triangles + .setFrontFace(vk::FrontFace::eCounterClockwise) // Counter-clockwise vertices = front-facing + .setDepthBiasEnable(false); // No depth bias (used for shadow mapping) +---- + +The rasterization configuration directly impacts both rendering performance and visual quality. Back-face culling provides a significant performance boost by eliminating triangles that face away from the camera, effectively halving the fragment processing workload for typical closed meshes. The counter-clockwise winding order follows the standard convention used by most 3D modeling tools and asset pipelines. + +Fill mode produces solid triangles appropriate for PBR rendering, though wireframe mode can be useful for debugging geometry or creating special visual effects. The line width setting only affects wireframe rendering, but some graphics drivers require it to be specified even when using fill mode. + +Depth bias (also known as polygon offset) is commonly used in shadow mapping to prevent self-shadowing artifacts, but it's unnecessary for standard forward rendering and can introduce its own artifacts if used inappropriately. + +=== Multisampling and Anti-Aliasing + +The multisampling configuration determines how the GPU handles anti-aliasing to reduce visual artifacts from geometric edges. + +[source,cpp] +---- + // Configure multisampling - anti-aliasing settings vk::PipelineMultisampleStateCreateInfo multisampling; - multisampling.setSampleShadingEnable(false) - .setRasterizationSamples(vk::SampleCountFlagBits::e1); + multisampling.setSampleShadingEnable(false) // Disable per-sample shading + .setRasterizationSamples(vk::SampleCountFlagBits::e1); // No multisampling (1 sample per pixel) +---- + +This configuration disables multisampling anti-aliasing (MSAA) for simplicity and performance. While MSAA can significantly improve visual quality by reducing aliasing artifacts on geometric edges, it also substantially increases memory bandwidth requirements and fragment processing costs. For learning purposes and initial implementations, single-sample rendering provides a good balance between performance and complexity. - // Depth and stencil state +In production applications, you might enable MSAA by increasing the sample count to 4x or 8x, depending on performance requirements and target hardware capabilities. Per-sample shading, when enabled, runs the fragment shader once per sample rather than once per pixel, providing the highest quality anti-aliasing at the cost of proportionally increased fragment processing time. + +=== Phase 7: Depth Testing and Z-Buffer Configuration + +The depth and stencil state configuration controls how fragments interact with the depth buffer to achieve proper depth sorting and occlusion. + +[source,cpp] +---- + // Configure depth and stencil testing vk::PipelineDepthStencilStateCreateInfo depthStencil; - depthStencil.setDepthTestEnable(true) - .setDepthWriteEnable(true) - .setDepthCompareOp(vk::CompareOp::eLess) - .setDepthBoundsTestEnable(false) - .setStencilTestEnable(false); + depthStencil.setDepthTestEnable(true) // Enable depth testing for proper occlusion + .setDepthWriteEnable(true) // Write depth values to depth buffer + .setDepthCompareOp(vk::CompareOp::eLess) // Fragment passes if its depth is less (closer) + .setDepthBoundsTestEnable(false) // Don't use depth bounds testing + .setStencilTestEnable(false); // Don't use stencil testing +---- + +Depth testing forms the foundation of proper 3D rendering by ensuring that closer objects occlude more distant ones. The "less than" comparison function works with the standard depth buffer convention where smaller depth values represent closer fragments. This configuration writes depth values for each rendered fragment, building up the depth buffer that subsequent draw calls can use for occlusion testing. + +Depth bounds testing and stencil testing are advanced features used for specific rendering techniques like light volume optimization or complex compositing operations. For standard PBR rendering, they add unnecessary complexity without providing benefits, so we disable them to maintain optimal performance. + +=== Phase 8: Color Blending and Transparency - // Color blend state +The color blend state determines how new fragments combine with existing color values in the framebuffer, enabling transparency and various compositing effects. + +[source,cpp] +---- + // Configure color blending - how new pixels combine with existing ones vk::PipelineColorBlendAttachmentState colorBlendAttachment; colorBlendAttachment.setColorWriteMask( - vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | // Write all color channels vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA) - .setBlendEnable(true) - .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) - .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) - .setColorBlendOp(vk::BlendOp::eAdd) - .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) - .setDstAlphaBlendFactor(vk::BlendFactor::eZero) - .setAlphaBlendOp(vk::BlendOp::eAdd); + .setBlendEnable(true) // Enable alpha blending + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) // New fragment's alpha + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) // One minus new fragment's alpha + .setColorBlendOp(vk::BlendOp::eAdd) // Add source and destination + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) // Preserve new alpha + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) // Ignore old alpha + .setAlphaBlendOp(vk::BlendOp::eAdd); // Add alpha values vk::PipelineColorBlendStateCreateInfo colorBlending; - colorBlending.setLogicOpEnable(false) - .setAttachmentCount(1) + colorBlending.setLogicOpEnable(false) // Don't use logical operations + .setAttachmentCount(1) // Single color attachment .setPAttachments(&colorBlendAttachment); +---- - // Set up push constant range for material properties +This blend configuration implements standard alpha transparency using the classic "over" compositing operation. The formula `(srcAlpha * newColor) + ((1 - srcAlpha) * oldColor)` produces natural-looking transparency effects where fully opaque fragments (alpha = 1) completely replace the background, while partially transparent fragments blend proportionally. + +The separate alpha blending configuration preserves the alpha channel properly for potential multi-pass rendering or post-processing effects. By setting source alpha factor to one and destination alpha factor to zero, we ensure that the final alpha value comes entirely from the new fragment, which is typically the desired behavior for transparency effects. + +=== Phase 9: Pipeline Layout and Resource Binding + +The pipeline layout defines how resources like textures, uniform buffers, and push constants are organized and accessed by the shaders. + +[source,cpp] +---- + // Configure push constants for fast material property updates vk::PushConstantRange pushConstantRange; - pushConstantRange.setStageFlags(vk::ShaderStageFlagBits::eFragment) - .setOffset(0) - .setSize(sizeof(PushConstantBlock)); // Size of our push constant data + pushConstantRange.setStageFlags(vk::ShaderStageFlagBits::eFragment) // Only fragment shader uses these + .setOffset(0) // Start at beginning + .setSize(sizeof(PushConstantBlock)); // Size of our material data - // Create pipeline layout with push constants + // Create the pipeline layout - defines resource organization vk::PipelineLayoutCreateInfo pipelineLayoutInfo; - pipelineLayoutInfo.setSetLayoutCount(1) - .setPSetLayouts(&*descriptorSetLayout) - .setPushConstantRangeCount(1) + pipelineLayoutInfo.setSetLayoutCount(1) // Single descriptor set + .setPSetLayouts(&*descriptorSetLayout) // Our texture/uniform bindings + .setPushConstantRangeCount(1) // One push constant block .setPPushConstantRanges(&pushConstantRange); - // Create pipeline layout with vk::raii + // Create the pipeline layout object pbrPipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); +---- + +The pipeline layout serves as a contract between the application and shaders regarding resource organization. Push constants provide the fastest path for updating small amounts of data (like material properties) between draw calls, as they bypass the memory hierarchy and are directly accessible to shader cores. The 128-byte limit on push constants in most implementations makes them perfect for per-material data but unsuitable for larger datasets. + +The descriptor set layout reference connects our pipeline to the texture and uniform buffer bindings we established earlier. This separation of concerns allows the same descriptor set layout to be used across multiple pipelines while maintaining clean resource organization. - // Create the PBR graphics pipeline +=== Phase 10: Final Pipeline Creation and Dynamic Rendering Setup + +The final phase assembles all configuration states into a complete graphics pipeline and sets up dynamic rendering compatibility for modern Vulkan applications. + +[source,cpp] +---- + // Assemble the complete graphics pipeline vk::GraphicsPipelineCreateInfo pipelineInfo; - pipelineInfo.setStageCount(static_cast(shaderStages.size())) - .setPStages(shaderStages.data()) - .setPVertexInputState(&vertexInputInfo) - .setPInputAssemblyState(&inputAssembly) - .setPViewportState(&viewportState) - .setPRasterizationState(&rasterizer) - .setPMultisampleState(&multisampling) - .setPDepthStencilState(&depthStencil) - .setPColorBlendState(&colorBlending) - .setPDynamicState(&dynamicState) - .setLayout(*pbrPipelineLayout) - .setRenderPass(nullptr) // Using dynamic rendering - .setSubpass(0) - .setBasePipelineHandle(nullptr); - - // Set up dynamic rendering info + pipelineInfo.setStageCount(static_cast(shaderStages.size())) // Number of shader stages + .setPStages(shaderStages.data()) // Shader stage configurations + .setPVertexInputState(&vertexInputInfo) // Vertex format + .setPInputAssemblyState(&inputAssembly) // Primitive topology + .setPViewportState(&viewportState) // Viewport configuration + .setPRasterizationState(&rasterizer) // Rasterization settings + .setPMultisampleState(&multisampling) // Anti-aliasing settings + .setPDepthStencilState(&depthStencil) // Depth/stencil testing + .setPColorBlendState(&colorBlending) // Blending configuration + .setPDynamicState(&dynamicState) // Dynamic state settings + .setLayout(*pbrPipelineLayout) // Resource layout + .setRenderPass(nullptr) // Using dynamic rendering + .setSubpass(0) // Subpass index + .setBasePipelineHandle(nullptr); // No base pipeline + + // Configure for dynamic rendering (modern Vulkan approach) vk::PipelineRenderingCreateInfo renderingInfo; - renderingInfo.setColorAttachmentCount(1) - .setPColorAttachmentFormats(&swapChainImageFormat) - .setDepthAttachmentFormat(findDepthFormat()); + renderingInfo.setColorAttachmentCount(1) // Single color target + .setPColorAttachmentFormats(&swapChainImageFormat) // Match swapchain format + .setDepthAttachmentFormat(findDepthFormat()); // Depth buffer format pipelineInfo.setPNext(&renderingInfo); - // Create graphics pipeline with vk::raii + // Create the final graphics pipeline pbrPipeline = device.createGraphicsPipeline(nullptr, pipelineInfo); - // With vk::raii, shader module is automatically destroyed when it goes out of scope - return true; } catch (const std::exception& e) { std::cerr << "Error creating PBR pipeline: " << e.what() << std::endl; @@ -409,6 +680,12 @@ bool Renderer::createPBRPipeline() { } ---- +The pipeline creation represents the culmination of all our configuration work, where Vulkan validates the entire pipeline specification and compiles it into an optimized form suitable for GPU execution. The dynamic rendering configuration replaces the traditional render pass system with a more flexible approach that allows render targets to be specified at command recording time rather than pipeline creation time. + +This flexibility proves particularly valuable for applications that need to render to different targets (like shadow maps, reflection textures, or post-processing buffers) using the same pipeline. The format specifications ensure that the pipeline generates output compatible with our target render surfaces. + +The exception handling provides essential feedback during development, as pipeline creation failures can result from subtle configuration mismatches or resource compatibility issues that are difficult to debug without proper error reporting. + This function creates a new pipeline for our PBR shader, including support for push constants. We'll also need to update our uniform buffer to include light information: [source,cpp] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc index 7ddb1037..8bdbffe0 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc @@ -2,7 +2,21 @@ In this section, we'll integrate our PBR implementation with the rest of the Vulkan rendering pipeline. We'll update our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. The techniques we develop here will be applied in the Loading_Models chapter when we load and render glTF models. -Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. +To keep the flow concrete and avoid repeating earlier theory, use this quick roadmap: + +1) Extend the renderer with PBR pipeline objects and a material push-constant block +2) Create the PBR pipeline (layout, shaders, blending, formats) alongside the main pipeline +3) Record draws: bind PBR pipeline, bind geometry, and push per-material constants per mesh +4) Clean up via RAII (no special teardown required) + +[NOTE] +==== +We won’t re-explain PBR theory or push-constant fundamentals here. See Lighting_Materials/03_push_constants.adoc for push constants and Lighting_Materials/01_introduction.adoc (and 05_pbr_rendering.adoc) for PBR concepts. +==== + +The PBR pass slots into the graphics pipeline as shown below: + +image::../../../images/rendering_pipeline_flowchart.png[Rendering Pipeline Flowchart, width=600, alt=Rendering pipeline flowchart showing where the PBR pass fits] == Updating the Renderer Class @@ -132,71 +146,65 @@ void Renderer::recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t ima == Creating the PBR Shader -Now that we've updated our renderer to support our PBR implementation, we need to create the PBR shader that implements the concepts we've discussed in this chapter. Let's create a file called `pbr.slang` in our shaders directory: +Now that we've updated our renderer to support our PBR implementation, we need to create the PBR shader that implements the concepts we've discussed in this chapter. Rather than presenting this as a monolithic code dump, let's break down the shader creation into logical sections that explain both the technical implementation and the reasoning behind each component. + +=== Section 1: Shader Structure and Data Interface + +The first section establishes the communication interface between our CPU application and GPU shader, defining how data flows through the rendering pipeline. [source,cpp] ---- -// Let's create our PBR shader based on the concepts we've learned -// We can create the shader file programmatically: -// -// std::ofstream shaderFile("shaders/pbr.slang"); -// shaderFile << R"( -// -// Or we can create it manually in our shaders directory. -// -// Here's what our PBR shader looks like: -// // Combined vertex and fragment shader for PBR rendering -// Input from vertex buffer +// Input from vertex buffer - Data sent per vertex from CPU struct VSInput { - float3 Position : POSITION; - float3 Normal : NORMAL; - float2 UV : TEXCOORD0; - float4 Tangent : TANGENT; + float3 Position : POSITION; // 3D position in model space + float3 Normal : NORMAL; // Surface normal for lighting calculations + float2 UV : TEXCOORD0; // Texture coordinates for material sampling + float4 Tangent : TANGENT; // Tangent vector for normal mapping (w component = handedness) }; -// Output from vertex shader / Input to fragment shader +// Output from vertex shader / Input to fragment shader - Interpolated data struct VSOutput { - float4 Position : SV_POSITION; - float3 WorldPos : POSITION; - float3 Normal : NORMAL; - float2 UV : TEXCOORD0; - float4 Tangent : TANGENT; + float4 Position : SV_POSITION; // Required clip space position for rasterization + float3 WorldPos : POSITION; // World space position for lighting calculations + float3 Normal : NORMAL; // World space normal (interpolated) + float2 UV : TEXCOORD0; // Texture coordinates (interpolated) + float4 Tangent : TANGENT; // World space tangent (interpolated) }; -// Uniform buffer +// Uniform buffer - Global data shared across all vertices/fragments struct UniformBufferObject { - float4x4 model; - float4x4 view; - float4x4 proj; - float4 lightPositions[4]; - float4 lightColors[4]; - float4 camPos; - float exposure; - float gamma; - float prefilteredCubeMipLevels; - float scaleIBLAmbient; + float4x4 model; // Model-to-world transformation matrix + float4x4 view; // World-to-camera transformation matrix + float4x4 proj; // Camera-to-clip space projection matrix + float4 lightPositions[4]; // Light positions in world space + float4 lightColors[4]; // Light intensities and colors + float4 camPos; // Camera position for view-dependent effects + float exposure; // HDR exposure control + float gamma; // Gamma correction value (typically 2.2) + float prefilteredCubeMipLevels; // IBL prefiltered environment map mip levels + float scaleIBLAmbient; // IBL ambient contribution scale }; -// Push constants for material properties +// Push constants - Fast, small data updated frequently per material/object struct PushConstants { - float4 baseColorFactor; - float metallicFactor; - float roughnessFactor; - int baseColorTextureSet; - int physicalDescriptorTextureSet; - int normalTextureSet; - int occlusionTextureSet; - int emissiveTextureSet; - float alphaMask; - float alphaMaskCutoff; + float4 baseColorFactor; // Base color tint/multiplier + float metallicFactor; // Metallic property multiplier + float roughnessFactor; // Surface roughness multiplier + int baseColorTextureSet; // Texture binding index for base color (-1 = none) + int physicalDescriptorTextureSet; // Texture binding for metallic/roughness + int normalTextureSet; // Texture binding for normal maps + int occlusionTextureSet; // Texture binding for ambient occlusion + int emissiveTextureSet; // Texture binding for emissive maps + float alphaMask; // Alpha masking enable flag + float alphaMaskCutoff; // Alpha cutoff threshold }; -// Constants +// Mathematical constants static const float PI = 3.14159265359; -// Bindings +// Resource bindings - Connect CPU resources to GPU shader registers [[vk::binding(0, 0)]] ConstantBuffer ubo; [[vk::binding(1, 0)]] Texture2D baseColorMap; [[vk::binding(1, 0)]] SamplerState baseColorSampler; @@ -210,143 +218,205 @@ static const float PI = 3.14159265359; [[vk::binding(5, 0)]] SamplerState emissiveSampler; [[vk::push_constant]] PushConstants material; +---- -// PBR functions +This interface design reflects modern GPU architecture principles where different types of data flow through different pathways based on their update frequency and size. Uniform buffers efficiently handle large, infrequently changing data like transformation matrices, while push constants provide ultra-fast updates for small, frequently changing material properties. + +=== Section 2: PBR Mathematical Foundation + +The second section implements the core mathematical functions that form the foundation of physically-based rendering, translating complex light-surface interactions into computationally efficient approximations. + +[source,cpp] +---- +// Normal Distribution Function (D) - GGX/Trowbridge-Reitz Distribution +// Describes the statistical distribution of microfacet orientations float DistributionGGX(float NdotH, float roughness) { - float a = roughness * roughness; + float a = roughness * roughness; // Remapping for more perceptual linearity float a2 = a * a; float NdotH2 = NdotH * NdotH; - float nom = a2; + float nom = a2; // Numerator: concentration factor float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; + denom = PI * denom * denom; // Normalization factor - return nom / denom; + return nom / denom; // Normalized distribution } +// Geometry Function (G) - Smith's method with Schlick-GGX approximation +// Models self-shadowing and masking between microfacets float GeometrySmith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; - float k = (r * r) / 8.0; + float k = (r * r) / 8.0; // Direct lighting remapping + // Geometry obstruction from view direction (masking) float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + // Geometry obstruction from light direction (shadowing) float ggx2 = NdotL / (NdotL * (1.0 - k) + k); - return ggx1 * ggx2; + return ggx1 * ggx2; // Combined masking-shadowing } +// Fresnel Reflectance (F) - Schlick's approximation +// Models how reflectance changes with viewing angle float3 FresnelSchlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } +---- + +These mathematical functions represent decades of computer graphics research distilled into efficient real-time approximations. The GGX distribution provides more realistic highlight falloff compared to older models, while the Smith geometry function ensures energy conservation at grazing angles. The Fresnel approximation captures the essential angle-dependent reflection behavior that makes materials look convincing under different viewing conditions. -// Vertex shader entry point +=== Section 3: Vertex and Fragment Shader Implementation + +The final section contains the actual shader entry points that execute for each vertex and pixel, implementing the complete PBR pipeline from geometry transformation through final color output. + +[source,cpp] +---- +// Vertex shader entry point - Executes once per vertex [[shader("vertex")]] VSOutput VSMain(VSInput input) { VSOutput output; - // Transform position to clip space + // Transform vertex position through the rendering pipeline + // Model -> World -> Camera -> Clip space transformation chain float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); - // Pass world position to fragment shader + // Pass world position for fragment lighting calculations + // Fragment shader needs world space position to calculate light vectors output.WorldPos = worldPos.xyz; - // Transform normal to world space + // Transform normal from model space to world space + // Use only rotation/scale part of model matrix (upper-left 3x3) + // Normalize to ensure unit length after transformation output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); - // Pass texture coordinates + // Pass through texture coordinates unchanged + // UV coordinates are typically in [0,1] range and don't need transformation output.UV = input.UV; - // Pass tangent + // Pass tangent vector for normal mapping + // Will be used in fragment shader to construct tangent-space basis output.Tangent = input.Tangent; return output; } -// Fragment shader entry point +// Fragment shader entry point - Executes once per pixel [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET { - // Sample material textures + // === MATERIAL PROPERTY SAMPLING === + // Sample base color texture and apply material color factor float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + + // Sample metallic-roughness texture (metallic=B channel, roughness=G channel) + // glTF standard: metallic stored in blue, roughness in green float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; float metallic = metallicRoughness.x * material.metallicFactor; float roughness = metallicRoughness.y * material.roughnessFactor; + + // Sample ambient occlusion (typically stored in red channel) float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; + + // Sample emissive texture for self-illuminating materials float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; - // Calculate normal in tangent space + // === NORMAL CALCULATION === + // Start with interpolated surface normal float3 N = normalize(input.Normal); + + // Apply normal mapping if texture is available if (material.normalTextureSet >= 0) { - // Apply normal mapping + // Sample normal map and convert from [0,1] to [-1,1] range float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; - float3 T = normalize(input.Tangent.xyz); - float3 B = normalize(cross(N, T)) * input.Tangent.w; - float3x3 TBN = float3x3(T, B, N); + + // Construct tangent-space to world-space transformation matrix (TBN) + float3 T = normalize(input.Tangent.xyz); // Tangent + float3 B = normalize(cross(N, T)) * input.Tangent.w; // Bitangent (w = handedness) + float3x3 TBN = float3x3(T, B, N); // Tangent-Bitangent-Normal matrix + + // Transform normal from tangent space to world space N = normalize(mul(tangentNormal, TBN)); } - // Calculate view and reflection vectors + // === LIGHTING SETUP === + // Calculate view direction (camera to fragment) float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + + // Calculate reflection vector for environment mapping float3 R = reflect(-V, N); - // Calculate F0 (base reflectivity) - float3 F0 = float3(0.04, 0.04, 0.04); - F0 = lerp(F0, baseColor.rgb, metallic); + // === PBR MATERIAL SETUP === + // Calculate F0 (reflectance at normal incidence) + // Non-metals: low reflectance (~0.04), Metals: colored reflectance from base color + float3 F0 = float3(0.04, 0.04, 0.04); // Dielectric default + F0 = lerp(F0, baseColor.rgb, metallic); // Lerp to metallic behavior - // Initialize lighting + // Initialize outgoing radiance accumulator float3 Lo = float3(0.0, 0.0, 0.0); - // Calculate lighting for each light + // === DIRECT LIGHTING LOOP === + // Calculate contribution from each light source for (int i = 0; i < 4; i++) { float3 lightPos = ubo.lightPositions[i].xyz; float3 lightColor = ubo.lightColors[i].rgb; - // Calculate light direction and distance - float3 L = normalize(lightPos - input.WorldPos); - float distance = length(lightPos - input.WorldPos); - float attenuation = 1.0 / (distance * distance); - float3 radiance = lightColor * attenuation; + // Calculate light direction and attenuation + float3 L = normalize(lightPos - input.WorldPos); // Light direction + float distance = length(lightPos - input.WorldPos); // Distance for falloff + float attenuation = 1.0 / (distance * distance); // Inverse square falloff + float3 radiance = lightColor * attenuation; // Attenuated light color - // Calculate half vector + // Calculate half vector (between view and light directions) float3 H = normalize(V + L); - // Calculate BRDF terms - float NdotL = max(dot(N, L), 0.0); - float NdotV = max(dot(N, V), 0.0); - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); + // === BRDF EVALUATION === + // Calculate all necessary dot products for BRDF terms + float NdotL = max(dot(N, L), 0.0); // Lambertian falloff + float NdotV = max(dot(N, V), 0.0); // View angle + float NdotH = max(dot(N, H), 0.0); // Half vector for specular + float HdotV = max(dot(H, V), 0.0); // For Fresnel calculation - // Specular BRDF - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmith(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(HdotV, F0); + // Evaluate Cook-Torrance BRDF components + float D = DistributionGGX(NdotH, roughness); // Normal distribution + float G = GeometrySmith(NdotV, NdotL, roughness); // Geometry function + float3 F = FresnelSchlick(HdotV, F0); // Fresnel reflectance + // Calculate specular BRDF float3 numerator = D * G * F; - float denominator = 4.0 * NdotV * NdotL + 0.0001; + float denominator = 4.0 * NdotV * NdotL + 0.0001; // Prevent division by zero float3 specular = numerator / denominator; - // Energy conservation - float3 kS = F; - float3 kD = float3(1.0, 1.0, 1.0) - kS; - kD *= 1.0 - metallic; + // === ENERGY CONSERVATION === + // Fresnel term represents specular reflection ratio + float3 kS = F; // Specular contribution + float3 kD = float3(1.0, 1.0, 1.0) - kS; // Diffuse contribution (energy conservation) + kD *= 1.0 - metallic; // Metals have no diffuse reflection - // Add to outgoing radiance + // === RADIANCE ACCUMULATION === + // Combine diffuse (Lambertian) and specular (Cook-Torrance) terms + // Multiply by incident radiance and cosine foreshortening Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; } - // Add ambient and emissive + // === AMBIENT AND EMISSIVE === + // Add simple ambient lighting (should be replaced with IBL in production) float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + + // Combine all lighting contributions float3 color = ambient + Lo + emissive; - // HDR tonemapping and gamma correction + // === HDR TONE MAPPING AND GAMMA CORRECTION === + // Apply Reinhard tone mapping to compress HDR values to [0,1] range color = color / (color + float3(1.0, 1.0, 1.0)); + + // Apply gamma correction for sRGB display (inverse gamma) color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + // Output final color with original alpha return float4(color, baseColor.a); } -)"; -shaderFile.close(); ---- == Compiling the Shader diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc index cf8f0657..c3e237b4 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc @@ -2,47 +2,40 @@ In this chapter, we've explored the fundamentals of lighting and materials in 3D rendering and introduced Physically Based Rendering (PBR) using the metallic-roughness workflow. We've covered the theory behind PBR and implemented a shader that can be used with glTF models. We've also learned how to use push constants to efficiently pass material properties to our shaders. -Throughout our engine implementation, we've used vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. - == What We've Learned -Throughout this chapter, we've covered: - -1. *Physically Based Rendering*: We've deepened our understanding of the core principles of PBR, including energy conservation, microfacet theory, and the Fresnel effect, and how they combine to create physically accurate lighting. - -2. *glTF Material Integration*: We've learned how to leverage the PBR materials defined in glTF files, taking advantage of the metallic-roughness workflow that glTF uses. +This chapter has taken you through the essential concepts needed to implement physically-based rendering in a Vulkan engine. We introduced the metallic‑roughness PBR workflow, mapped glTF material properties to shader inputs, and used push constants to drive per‑draw material parameters without descriptor churn. You saw how the BRDF pieces cooperate to conserve energy and produce plausible lighting, and how to plug the shader into a vk::raii‑based pipeline so models render correctly end‑to‑end. -3. *Push Constants*: We've explored push constants, a powerful feature in Vulkan that allows us to efficiently pass material properties to shaders without the overhead of descriptor sets. +== Making it click: a mental model of this PBR pipeline -4. *Advanced Shader Techniques*: We've examined and extended the PBR shader implementation, understanding how each component contributes to realistic rendering. - -5. *Vulkan Integration*: We've integrated our enhanced PBR understanding with the rest of the Vulkan rendering pipeline, updating our renderer class to work seamlessly with glTF models and their PBR materials. - -== Potential Improvements +At a high level, think of your frame as a linear-light computation that transforms physical inputs into displayable pixels: -While our PBR implementation is functional, there are several ways it could be improved: +- Inputs in linear space: lights with intensities in physical-ish units, baseColor/metallic/roughness from material, and normal/AO/emissive maps. The work is done in linear HDR so you don’t lose headroom. +- BRDF roles: D shapes the highlight (roughness controls lobe width), G enforces masking/self-shadowing on microfacets, F boosts reflectance at grazing angles and ties reflectivity to material type via F0. Energy conservation links specular (kS) and diffuse (kD) so total doesn’t exceed what came in. +- Material knobs as levers: + * Roughness: widens/narrows the specular lobe and also reduces peak intensity via G. + * Metallic: cross-fades between dielectric behavior (colored diffuse + neutral specular) and conductor behavior (colored specular, no diffuse). + * Base color: is diffuse albedo for dielectrics and colored specular for metals. +- Normal/AO/emissive context: normal maps perturb local orientation to add detail, AO damps indirect/ambient to avoid flat crevices, emissive adds light-independent glow. +- Output staging: after summing ambient/indirect and direct lighting, compress HDR with a tone mapper (e.g., Reinhard/ACES) and only then apply gamma to match the display. Do gamma exactly once (either shader pow or sRGB framebuffer). -1. *Image-Based Lighting (IBL)*: Adding environment maps for ambient lighting would greatly enhance the realism of our scenes, especially for reflective materials. +A quick reasoning loop when results look off: -2. *Shadow Mapping*: Adding shadow mapping would greatly enhance the realism of our scenes. +1. Confirm spaces and order: linear lighting ➜ tone map ➜ gamma. Check you’re not doing double-gamma. +2. Probe the BRDF: plastic look on everything? Roughness too low or kD not reduced by metallic. Dim, muddy highlights? Roughness too high or exposure too low. +3. Validate normals/TBN: inverted green channel or wrong tangent handedness causes odd shading and seams. +4. Calibrate exposure/tone map: if whites clip harshly, add exposure control and/or switch to ACES/Hable for smoother roll-off. +5. Use AO and emissive judiciously: AO should affect ambient/IBL, not direct specular; emissive is additive and independent of lights. -3. *Advanced Material Features*: Implementing features like clear coat, anisotropy, and subsurface scattering would allow for a wider range of material types. +This mental model helps you predict how a change to any input will echo through the pipeline and appear on screen, which is the core of “understanding,” not just following a list. -4. *Optimizations*: Implementing techniques like deferred rendering or clustered forward rendering would improve performance with many light sources. +== Potential Improvements -5. *HDR Pipeline*: Expanding our HDR pipeline with features like bloom, lens flares, and more sophisticated tone mapping operators. +Our PBR pass is a solid baseline. The most impactful upgrades are image‑based lighting (environment maps for ambient/indirect), shadowing, and a few material extensions (e.g., clear coat or anisotropy). On the performance side, consider clustered forward or a deferred path when light counts grow. If you build an HDR chain, bloom and a more filmic tone mapper (ACES/Hable) round out the presentation. == Next Steps -Now that you have a solid understanding of PBR and materials, you might want to explore: - -1. *Advanced Lighting Techniques*: Research more advanced lighting techniques like global illumination, ambient occlusion, and volumetric lighting. - -2. *Material Systems*: Develop a more sophisticated material system that can handle a wider range of material properties and effects. - -3. *Shader Effects*: Experiment with different shader effects like fog, bloom, and depth of field. - -4. *Performance Optimization*: Optimize your lighting calculations for better performance, especially for mobile devices or scenes with many light sources. +Pick one thread and go deep. For lighting, explore GI/AO/volumetrics as time allows. For materials, design a data‑driven system that maps glTF (and custom) parameters cleanly to your shaders. For visuals, prototype post effects (fog, bloom, DoF). For performance, profile first, then optimize the hot spots—especially on mobile. Remember that lighting is a complex topic with many approaches and techniques. The implementation we've covered in this chapter is just the beginning. As you continue to develop your engine, you'll likely want to refine and expand your lighting system to meet the specific needs of your projects. diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc index efc0c932..62d33ed2 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc @@ -1,13 +1,6 @@ -::pp: {plus}{plus} +:pp: {plus}{plus} = Lighting & Materials: Basic lighting models and push constants -:doctype: book -:sectnums: -:sectnumlevels: 4 -:toc: left -:icons: font -:source-highlighter: highlightjs -:source-language: c++ This chapter covers the implementation of basic lighting models and the use of push constants for material properties in Vulkan. Throughout our engine implementation, we use vk::raii dynamic rendering and C++20 modules to create a modern, efficient, and maintainable codebase. diff --git a/images/bistro.png b/images/bistro.png new file mode 100644 index 0000000000000000000000000000000000000000..7dbe9cf841ccbbb590c821f881f550a291e1cb89 GIT binary patch literal 735878 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%0AcmuV>hcGqsWRn~oK{XhtyYL@Zy^_jm_4q`Gj?NB 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