diff --git a/.gitignore b/.gitignore index 615bac335..af6b34dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ compiled.spv */.vscode/* */__main__.py tmp +/tmp/rtSamples.bin +/runtime/ diff --git a/09_GeometryCreator/include/common.hpp b/09_GeometryCreator/include/common.hpp index 84cd8118a..b2005ec50 100644 --- a/09_GeometryCreator/include/common.hpp +++ b/09_GeometryCreator/include/common.hpp @@ -3,6 +3,7 @@ #include "nbl/examples/examples.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" using namespace nbl; using namespace core; @@ -15,4 +16,4 @@ using namespace scene; using namespace nbl::examples; -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file +#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/09_GeometryCreator/main.cpp b/09_GeometryCreator/main.cpp index 6e34a9064..5c45213c9 100644 --- a/09_GeometryCreator/main.cpp +++ b/09_GeometryCreator/main.cpp @@ -72,10 +72,15 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes // camera { - core::vectorSIMDf cameraPosition(-5.81655884, 2.58630896, -4.23974705); - core::vectorSIMDf cameraTarget(-0.349590302, -0.213266611, 0.317821503); - float32_t4x4 projectionMatrix = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(60.0f), float(m_initialResolution.x) / m_initialResolution.y, 0.1f, 10000.0f); - camera = Camera(cameraPosition, cameraTarget, projectionMatrix, 1.069f, 0.4f); + const auto cameraPosition = hlsl::float64_t3(-5.81655884, 2.58630896, -4.23974705); + const auto cameraTarget = hlsl::float64_t3(-0.349590302, -0.213266611, 0.317821503); + cameraProjection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(60.0f), float(m_initialResolution.x) / m_initialResolution.y, 0.1f, 10000.0f); + + camera = CCameraSimpleFPSUtilities::createFromLookAt(cameraPosition, cameraTarget, {1.069, 0.4}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; } onAppInitializedFinish(); @@ -94,10 +99,18 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); cb->beginDebugMarker("GeometryCreatorApp Frame"); { - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); mouseProcess(events); }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, m_logger.get()); - camera.endInputProcessing(nextPresentationTimestamp); + std::vector mouseEvents; + std::vector keyboardEvents; + + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { mouseEvents.insert(mouseEvents.end(), events.begin(), events.end()); }, m_logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { keyboardEvents.insert(keyboardEvents.end(), events.begin(), events.end()); }, m_logger.get()); + + mouseProcess({ mouseEvents.data(), mouseEvents.size() }); + + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents(mouseEvents, keyboardEvents, nextPresentationTimestamp, cameraInputRuntime, cameraInputConfig); + + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); } @@ -140,8 +153,8 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes cb->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); } - float32_t3x4 viewMatrix = camera.getViewMatrix(); - float32_t4x4 viewProjMatrix = camera.getConcatenatedMatrix(); + const auto viewMatrix = hlsl::float32_t3x4(camera->getGimbal().getViewMatrix()); + const auto viewProjMatrix = hlsl::math::linalg::promoted_mul(cameraProjection, viewMatrix); const auto viewParams = CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix); // tear down scene every frame @@ -247,16 +260,18 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes InputSystem::ChannelReader keyboard; // - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); uint16_t gcIndex = {}; - void mouseProcess(const nbl::ui::IMouseEventChannel::range_t& events) + void mouseProcess(std::span events) { - for (auto eventIt = events.begin(); eventIt != events.end(); eventIt++) + for (const auto& ev : events) { - auto ev = *eventIt; - if (ev.type==nbl::ui::SMouseEvent::EET_SCROLL && m_renderer) { gcIndex += int16_t(core::sign(ev.scrollEvent.verticalScroll)); diff --git a/0_ImportanceSamplingEnvMaps/main.cpp b/0_ImportanceSamplingEnvMaps/main.cpp index 30a75355f..8f562b4e9 100644 --- a/0_ImportanceSamplingEnvMaps/main.cpp +++ b/0_ImportanceSamplingEnvMaps/main.cpp @@ -4,9 +4,12 @@ #define _NBL_STATIC_LIB_ #include +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" -#include "CCamera.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" #include "../common/CommonAPI.h" using namespace nbl; @@ -122,7 +125,11 @@ class ImportanceSamplingEnvMaps : public ApplicationBase CommonAPI::InputSystem::ChannelReader mouse; CommonAPI::InputSystem::ChannelReader keyboard; - Camera camera = Camera(vectorSIMDf(0, 0, 0), vectorSIMDf(0, 0, 0), matrix4SIMD()); + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + nbl::examples::CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + nbl::examples::CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); core::smart_refctd_ptr gpuEnvmapPipeline; core::smart_refctd_ptr gpuEnvmapMeshBuffer; @@ -297,10 +304,14 @@ class ImportanceSamplingEnvMaps : public ApplicationBase logger = std::move(initOutput.logger); inputSystem = std::move(initOutput.inputSystem); - core::vectorSIMDf cameraPosition(-0.0889001, 0.678913, -4.01774); - core::vectorSIMDf cameraTarget(1.80119, 0.515374, -0.410544); - matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), float(WIN_W) / WIN_H, 0.03125f, 200.0f); - camera = Camera(cameraPosition, cameraTarget, projectionMatrix, 10.f, 1.f); + const auto cameraPosition = hlsl::float64_t3(-0.0889001, 0.678913, -4.01774); + const auto cameraTarget = hlsl::float64_t3(1.80119, 0.515374, -0.410544); + cameraProjection = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), float(WIN_W) / WIN_H, 0.03125f, 200.0f); + camera = nbl::examples::CCameraSimpleFPSUtilities::createFromLookAt(cameraPosition, cameraTarget, {10.0, 1.0}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; descriptorPool = createDescriptorPool(1u); @@ -775,10 +786,10 @@ class ImportanceSamplingEnvMaps : public ApplicationBase void onAppTerminated_impl() override { - const core::vectorSIMDf& last_cam_pos = camera.getPosition(); - const core::vectorSIMDf& last_cam_target = camera.getTarget(); - std::cout << "Last camera position: (" << last_cam_pos.X << ", " << last_cam_pos.Y << ", " << last_cam_pos.Z << ")" << std::endl; - std::cout << "Last camera target: (" << last_cam_target.X << ", " << last_cam_target.Y << ", " << last_cam_target.Z << ")" << std::endl; + const auto& lastCamPos = camera->getGimbal().getPosition(); + const auto lastCamTarget = camera->getGimbal().getWorldTarget(); + std::cout << "Last camera position: (" << lastCamPos.x << ", " << lastCamPos.y << ", " << lastCamPos.z << ")" << std::endl; + std::cout << "Last camera target: (" << lastCamTarget.x << ", " << lastCamTarget.y << ", " << lastCamTarget.z << ")" << std::endl; } void workLoopBody() override @@ -802,14 +813,23 @@ class ImportanceSamplingEnvMaps : public ApplicationBase inputSystem->getDefaultMouse(&mouse); inputSystem->getDefaultKeyboard(&keyboard); - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, logger.get()); - camera.endInputProcessing(nextPresentationTimestamp); + std::vector mouseEvents; + std::vector keyboardEvents; + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + { + mouseEvents.insert(mouseEvents.end(), events.begin(), events.end()); + }, logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + keyboardEvents.insert(keyboardEvents.end(), events.begin(), events.end()); + }, logger.get()); + const auto virtualEvents = nbl::examples::CCameraSimpleFPSUtilities::collectBasicVirtualEvents(mouseEvents, keyboardEvents, nextPresentationTimestamp, cameraInputRuntime, cameraInputConfig); + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); } - const auto& viewMatrix = camera.getViewMatrix(); - const auto& viewProjectionMatrix = camera.getConcatenatedMatrix(); + const auto viewMatrix = hlsl::float32_t3x4(camera->getGimbal().getViewMatrix()); + const auto viewProjectionMatrix = hlsl::math::linalg::promoted_mul(cameraProjection, viewMatrix); asset::SViewport viewport; viewport.minDepth = 1.f; @@ -883,4 +903,4 @@ class ImportanceSamplingEnvMaps : public ApplicationBase return windowCb->isWindowOpen(); } }; -NBL_COMMON_API_MAIN(ImportanceSamplingEnvMaps) \ No newline at end of file +NBL_COMMON_API_MAIN(ImportanceSamplingEnvMaps) diff --git a/12_MeshLoaders/include/common.hpp b/12_MeshLoaders/include/common.hpp index 84cd8118a..adf896c4d 100644 --- a/12_MeshLoaders/include/common.hpp +++ b/12_MeshLoaders/include/common.hpp @@ -3,6 +3,11 @@ #include "nbl/examples/examples.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" using namespace nbl; using namespace core; @@ -15,4 +20,4 @@ using namespace scene; using namespace nbl::examples; -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file +#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index e27ed4be0..03bca79eb 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -117,7 +117,8 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (!reloadModel()) return false; - camera.mapKeysToArrows(); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; onAppInitializedFinish(); return true; @@ -169,8 +170,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc // late latch input { bool reload = false; - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); + std::vector cameraMouseEvents; + std::vector cameraKeyboardEvents; + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + { + cameraMouseEvents.insert(cameraMouseEvents.end(), events.begin(), events.end()); + }, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { for (const auto& event : events) @@ -182,17 +187,19 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_drawBBMode = DrawBoundingBoxMode((m_drawBBMode + 1) % DBBM_COUNT); } } - camera.keyboardProcess(events); + cameraKeyboardEvents.insert(cameraKeyboardEvents.end(), events.begin(), events.end()); }, m_logger.get() ); - camera.endInputProcessing(nextPresentationTimestamp); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents(cameraMouseEvents, cameraKeyboardEvents, nextPresentationTimestamp, cameraInputRuntime, cameraInputConfig); + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); if (reload) reloadModel(); } // draw scene - float32_t3x4 viewMatrix = camera.getViewMatrix(); - float32_t4x4 viewProjMatrix = camera.getConcatenatedMatrix(); + float32_t3x4 viewMatrix = hlsl::float32_t3x4(camera->getGimbal().getViewMatrixRH()); + float32_t4x4 viewProjMatrix = hlsl::math::linalg::promoted_mul(cameraProjection, viewMatrix); m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); #ifdef NBL_BUILD_DEBUG_DRAW if (m_drawBBMode != DBBM_NONE) @@ -521,16 +528,20 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { const double distance = 0.05; const auto diagonal = bound.getExtent(); + const auto pos = bound.maxVx + diagonal * distance; + const auto center = (bound.minVx + bound.maxVx) * 0.5; { const auto measure = hlsl::length(diagonal); const auto aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); - camera.setProjectionMatrix(hlsl::math::thin_lens::rhPerspectiveFovMatrix(1.2f, aspectRatio, distance * measure * 0.1, measure * 4.0)); - camera.setMoveSpeed(measure * 0.04); + cameraProjection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(1.2f, aspectRatio, distance * measure * 0.1, measure * 4.0); + camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(pos.x, pos.y, pos.z), + hlsl::float64_t3(center.x, center.y, center.z), + {measure * 0.04, cameraRotateSpeed}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + cameraInputRuntime.binder = &cameraInputBinder; } - const auto pos = bound.maxVx + diagonal * distance; - camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); - const auto center = (bound.minVx + bound.maxVx) * 0.5; - camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); } // TODO: write out the geometry @@ -560,7 +571,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; // - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); + float cameraRotateSpeed = 1.0f; // mutables std::string m_modelPath; @@ -578,4 +594,4 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc nbl::system::path m_saveGeomPrefixPath; }; -NBL_MAIN_FUNC(MeshLoadersApp) \ No newline at end of file +NBL_MAIN_FUNC(MeshLoadersApp) diff --git a/31_HLSLPathTracer/include/nbl/this_example/common.hpp b/31_HLSLPathTracer/include/nbl/this_example/common.hpp index db051bb3e..80ee4d9e5 100644 --- a/31_HLSLPathTracer/include/nbl/this_example/common.hpp +++ b/31_HLSLPathTracer/include/nbl/this_example/common.hpp @@ -5,8 +5,12 @@ // common api #include "nbl/examples/common/SimpleWindowedApplication.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" #include "nbl/examples/examples.hpp" -#include "nbl/examples/cameras/CCamera.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" #include "nbl/examples/common/CEventCallback.hpp" // example's own headers @@ -14,4 +18,4 @@ #include "nbl/ext/ImGui/ImGui.h" #include "imgui/imgui_internal.h" -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file +#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/31_HLSLPathTracer/main.cpp b/31_HLSLPathTracer/main.cpp index d7569e4b8..409eb7adf 100644 --- a/31_HLSLPathTracer/main.cpp +++ b/31_HLSLPathTracer/main.cpp @@ -824,7 +824,7 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui ImGuizmo::SetRect(ImGui::GetWindowPos().x, ImGui::GetWindowPos().y, ImGui::GetWindowWidth(), ImGui::GetWindowHeight()); const auto aspectRatio = io.DisplaySize.x / io.DisplaySize.y; - m_camera.setProjectionMatrix(hlsl::math::thin_lens::rhPerspectiveFovMatrix(hlsl::radians(guiControlled.fov), aspectRatio, guiControlled.zNear, guiControlled.zFar)); + m_cameraProjection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(hlsl::radians(guiControlled.fov), aspectRatio, guiControlled.zNear, guiControlled.zFar); const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImVec2 viewportPos = viewport->Pos; @@ -1069,8 +1069,8 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui ImGuizmo::SetID(0u); - imguizmoM16InOut.view = hlsl::transpose(math::linalg::promoted_mul(float32_t4x4(1.f), m_camera.getViewMatrix())); - imguizmoM16InOut.projection = hlsl::transpose(m_camera.getProjectionMatrix()); + imguizmoM16InOut.view = hlsl::transpose(math::linalg::promoted_mul(float32_t4x4(1.f), hlsl::float32_t3x4(m_camera->getGimbal().getViewMatrixRH()))); + imguizmoM16InOut.projection = hlsl::transpose(m_cameraProjection); imguizmoM16InOut.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ m_transformParams.editTransformDecomposition = true; @@ -1112,9 +1112,17 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui // Set Camera { - core::vectorSIMDf cameraPosition(0, 5, -10); + const auto cameraPosition = hlsl::float64_t3(0.0, 5.0, -10.0); const auto proj = hlsl::math::thin_lens::rhPerspectiveFovMatrix(hlsl::radians(guiControlled.fov), WindowDimensions.x / WindowDimensions.y, guiControlled.zNear, guiControlled.zFar); - m_camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), proj); + m_cameraProjection = proj; + m_camera = CCameraSimpleFPSUtilities::createFromLookAt( + cameraPosition, + hlsl::float64_t3(0.0, 0.0, 0.0), + {guiControlled.moveSpeed, guiControlled.rotateSpeed}); + if (!m_camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(m_cameraInputBinder, *m_camera); + m_cameraInputRuntime.binder = &m_cameraInputBinder; } m_showUI = true; if (m_commandLine.ciMode) @@ -1124,8 +1132,6 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui m_surface->recreateSwapchain(); m_winMgr->show(m_window.get()); m_oracle.reportBeginFrameRecord(); - m_camera.mapKeysToArrows(); - // set initial rwmc settings resetRWMCParamsToDefaults(); applyLateCommandLineOverrides(); @@ -1213,7 +1219,7 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui RenderPushConstants pc; auto updatePathtracerPushConstants = [&]() -> void { // disregard surface/swapchain transformation for now - const float32_t4x4 viewProjectionMatrix = m_camera.getConcatenatedMatrix(); + const float32_t4x4 viewProjectionMatrix = hlsl::math::linalg::promoted_mul(m_cameraProjection, hlsl::float32_t3x4(m_camera->getGimbal().getViewMatrixRH())); const float32_t3x4 modelMatrix = hlsl::math::linalg::identity(); const float32_t4x4 modelViewProjectionMatrix = nbl::hlsl::math::linalg::promoted_mul(viewProjectionMatrix, modelMatrix); @@ -1536,8 +1542,7 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui inline void update() { - m_camera.setMoveSpeed(guiControlled.moveSpeed); - m_camera.setRotateSpeed(guiControlled.rotateSpeed); + CCameraSimpleFPSUtilities::applySpeedSettings(*m_camera, {guiControlled.moveSpeed, guiControlled.rotateSpeed}); static std::chrono::microseconds previousEventTimestamp{}; @@ -1561,9 +1566,10 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui { std::vector mouse{}; std::vector keyboard{}; + std::vector cameraMouse{}; + std::vector cameraKeyboard{}; } capturedEvents; - m_camera.beginInputProcessing(nextPresentationTimestamp); { if (!m_commandLine.ciMode) { @@ -1571,7 +1577,7 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { if (!io.WantCaptureMouse) - m_camera.mouseProcess(events); // don't capture the events, only let camera handle them with its impl + capturedEvents.cameraMouse.insert(capturedEvents.cameraMouse.end(), events.begin(), events.end()); for (const auto& e : events) // here capture { @@ -1586,10 +1592,10 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui } }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { if (!io.WantCaptureKeyboard) - m_camera.keyboardProcess(events); // don't capture the events, only let camera handle them with its impl + capturedEvents.cameraKeyboard.insert(capturedEvents.cameraKeyboard.end(), events.begin(), events.end()); for (const auto& e : events) // here capture { @@ -1615,7 +1621,14 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t&) -> void {}, m_logger.get()); } } - m_camera.endInputProcessing(nextPresentationTimestamp); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents( + capturedEvents.cameraMouse, + capturedEvents.cameraKeyboard, + nextPresentationTimestamp, + m_cameraInputRuntime, + m_cameraInputConfig); + if (!virtualEvents.empty()) + m_camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); const core::SRange mouseEvents(capturedEvents.mouse.data(), capturedEvents.mouse.data() + capturedEvents.mouse.size()); const core::SRange keyboardEvents(capturedEvents.keyboard.data(), capturedEvents.keyboard.data() + capturedEvents.keyboard.size()); @@ -2899,7 +2912,11 @@ class HLSLComputePathtracer final : public SimpleWindowedApplication, public Bui core::smart_refctd_ptr descriptorSet; } m_ui; - Camera m_camera; + core::smart_refctd_ptr m_camera; + ui::CGimbalInputBinder m_cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime m_cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig m_cameraInputConfig = {}; + hlsl::float32_t4x4 m_cameraProjection = hlsl::float32_t4x4(1.0f); bool m_showUI; bool m_exitRequested = false; bool m_sceneScreenshotRequested = false; diff --git a/34_DebugDraw/include/common.hpp b/34_DebugDraw/include/common.hpp index aad9bdb1d..db3aeea0e 100644 --- a/34_DebugDraw/include/common.hpp +++ b/34_DebugDraw/include/common.hpp @@ -3,7 +3,11 @@ #include -#include "nbl/examples/cameras/CCamera.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" #include "nbl/examples/common/SimpleWindowedApplication.hpp" #include "nbl/examples/common/CEventCallback.hpp" #include "nbl/examples/examples.hpp" @@ -19,4 +23,4 @@ using namespace ui; using namespace video; using namespace nbl::examples; -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file +#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/34_DebugDraw/main.cpp b/34_DebugDraw/main.cpp index f2dd6210d..9b9c781cc 100644 --- a/34_DebugDraw/main.cpp +++ b/34_DebugDraw/main.cpp @@ -57,8 +57,15 @@ class DebugDrawSampleApp final : public SimpleWindowedApplication, public Builti constexpr float fov = 60.f, zNear = 0.1f, zFar = 10000.f, moveSpeed = 1.f, rotateSpeed = 1.f; core::vectorSIMDf cameraPosition(14, 8, 12); core::vectorSIMDf cameraTarget(0, 0, 0); - hlsl::float32_t4x4 projectionMatrix = hlsl::math::thin_lens::rhPerspectiveFovMatrix(core::radians(fov), float(WIN_W) / WIN_H, zNear, zFar); - camera = Camera(cameraPosition, cameraTarget, projectionMatrix, moveSpeed, rotateSpeed); + cameraProjection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(core::radians(fov), float(WIN_W) / WIN_H, zNear, zFar); + camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(cameraPosition.x, cameraPosition.y, cameraPosition.z), + hlsl::float64_t3(cameraTarget.x, cameraTarget.y, cameraTarget.z), + {moveSpeed, rotateSpeed}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; } m_semaphore = m_device->createSemaphore(m_realFrameIx); @@ -190,10 +197,19 @@ class DebugDrawSampleApp final : public SimpleWindowedApplication, public Builti cmdbuf->beginDebugMarker("DebugDrawSampleApp IMGUI Frame"); { - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, m_logger.get()); - camera.endInputProcessing(nextPresentationTimestamp); + std::vector cameraMouseEvents; + std::vector cameraKeyboardEvents; + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + { + cameraMouseEvents.insert(cameraMouseEvents.end(), events.begin(), events.end()); + }, m_logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + cameraKeyboardEvents.insert(cameraKeyboardEvents.end(), events.begin(), events.end()); + }, m_logger.get()); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents(cameraMouseEvents, cameraKeyboardEvents, nextPresentationTimestamp, cameraInputRuntime, cameraInputConfig); + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); } auto* queue = getGraphicsQueue(); @@ -236,7 +252,7 @@ class DebugDrawSampleApp final : public SimpleWindowedApplication, public Builti ext::debug_draw::DrawAABB::DrawParameters drawParams; drawParams.commandBuffer = cmdbuf; - drawParams.cameraMat = camera.getConcatenatedMatrix(); + drawParams.cameraMat = hlsl::math::linalg::promoted_mul(cameraProjection, hlsl::float32_t3x4(camera->getGimbal().getViewMatrixRH())); if (!drawAABB->renderSingle(drawParams, testAABB, float32_t4{ 1, 0, 0, 1 })) m_logger->log("Unable to draw AABB with single draw pipeline!", ILogger::ELL_ERROR); @@ -357,11 +373,15 @@ class DebugDrawSampleApp final : public SimpleWindowedApplication, public Builti InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; - Camera camera; + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); video::CDumbPresentationOracle oracle; smart_refctd_ptr drawAABB; hlsl::shapes::AABB<3, float> testAABB = hlsl::shapes::AABB<3, float>{ { -5, -5, -5 }, { 10, 10, -10 } }; }; -NBL_MAIN_FUNC(DebugDrawSampleApp) \ No newline at end of file +NBL_MAIN_FUNC(DebugDrawSampleApp) diff --git a/45_BRDFEvalTest/main.cpp b/45_BRDFEvalTest/main.cpp index 69a92d1bc..9371f981b 100644 --- a/45_BRDFEvalTest/main.cpp +++ b/45_BRDFEvalTest/main.cpp @@ -7,7 +7,9 @@ #include #include -#include "CCamera.hpp" +#include +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" #include "../common/CommonAPI.h" #ifdef NBL_EMBED_BUILTIN_RESOURCES #include "example_data/builtin/CArchive.h" @@ -82,7 +84,8 @@ class BRDFEvalTestApp : public ApplicationBase { CommonAPI::InputSystem::ChannelReader mouse; CommonAPI::InputSystem::ChannelReader keyboard; - Camera camera; + core::smart_refctd_ptr camera; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); int resourceIx; uint32_t acquiredNextFBO = {}; @@ -100,10 +103,10 @@ class BRDFEvalTestApp : public ApplicationBase { struct SPushConsts { struct VertStage { - core::matrix4SIMD VP; + hlsl::float32_t4x4 VP; } vertStage; struct FragStage { - core::vectorSIMDf campos; + hlsl::float32_t4 campos; BRDFTestNumber testNum; uint32_t pad[3]; } fragStage; @@ -322,12 +325,14 @@ class BRDFEvalTestApp : public ApplicationBase { renderFinished[i] = logicalDevice->createSemaphore(); } - matrix4SIMD projectionMatrix = - matrix4SIMD::buildProjectionMatrixPerspectiveFovLH( - core::radians(60.0f), float(WIN_W) / WIN_H, 0.01f, 5000.0f); - camera = Camera(core::vectorSIMDf(6.75f, 2.f, 6.f), - core::vectorSIMDf(6.75f, 0.f, -1.f), projectionMatrix, 10.f, - 1.f); + cameraProjection = hlsl::math::thin_lens::lhPerspectiveFovMatrix( + core::radians(60.0f), float(WIN_W) / WIN_H, 0.01f, 5000.0f); + camera = nbl::examples::CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(6.75, 2.0, 6.0), + hlsl::float64_t3(6.75, 0.0, -1.0), + {10.0, 1.0}); + if (!camera) + return logFail("Could not initialize camera orientation!"); } void workLoopBody() override { @@ -417,8 +422,10 @@ class BRDFEvalTestApp : public ApplicationBase { commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); SPushConsts pc; - pc.vertStage.VP = camera.getConcatenatedMatrix(); - pc.fragStage.campos = core::vectorSIMDf(&camera.getPosition().X); + pc.vertStage.VP = hlsl::math::linalg::promoted_mul( + cameraProjection, + hlsl::float32_t3x4(camera->getGimbal().getViewMatrix())); + pc.fragStage.campos = hlsl::float32_t4(hlsl::CCameraMathUtilities::castVector(camera->getGimbal().getPosition()), 1.0f); pc.fragStage.testNum = currentTestNum; commandBuffer->pushConstants( gpuGraphicsPipeline->getRenderpassIndependentPipeline()->getLayout(), @@ -449,4 +456,4 @@ class BRDFEvalTestApp : public ApplicationBase { void onAppTerminated_impl() override { logicalDevice->waitIdle(); } }; -NBL_COMMON_API_MAIN(BRDFEvalTestApp) \ No newline at end of file +NBL_COMMON_API_MAIN(BRDFEvalTestApp) diff --git a/50.IESViewer/App.hpp b/50.IESViewer/App.hpp index 4912b4b2c..b713d1e61 100644 --- a/50.IESViewer/App.hpp +++ b/50.IESViewer/App.hpp @@ -10,6 +10,10 @@ #include "nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp" #include "nbl/examples/common/CEventCallback.hpp" #include "nbl/examples/common/InputSystem.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" #include "nbl/ui/ICursorControl.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/builtin/hlsl/cpp_compat.hlsl" @@ -251,7 +255,11 @@ class IESViewer final : public IESWindowedApplication, public BuiltinResourcesAp smart_refctd_ptr m_scene; smart_refctd_ptr m_renderer; - Camera camera; + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); uint32_t m_plot3DWidth = 640u; uint32_t m_plot3DHeight = 640u; float m_plotRadius = 100.0f; diff --git a/50.IESViewer/AppInit.cpp b/50.IESViewer/AppInit.cpp index 79338e8ec..a534d673e 100644 --- a/50.IESViewer/AppInit.cpp +++ b/50.IESViewer/AppInit.cpp @@ -460,12 +460,6 @@ bool IESViewer::onAppInitialized(smart_refctd_ptr&& system) float32_t4(0, 0, 1, 0) ); - using core_vec_t = std::remove_cv_t>; - const auto toCoreVec3 = [](const float32_t3& v) -> core_vec_t - { - return core_vec_t(v.x, v.y, v.z); - }; - float32_t3 cameraPosition(-5.81655884f, 2.58630896f, -4.23974705f); float32_t3 cameraTarget(-0.349590302f, -0.213266611f, 0.317821503f); const auto cameraOffset = cameraPosition - cameraTarget; @@ -474,9 +468,17 @@ bool IESViewer::onAppInitialized(smart_refctd_ptr&& system) const auto& params = m_frameBuffers3D.front()->getCreationParameters(); const float aspect = float(params.width) / float(params.height); const auto projectionMatrix = buildProjectionMatrixPerspectiveFovLH(hlsl::radians(uiState.cameraFovDeg), aspect, 0.1f, 10000.0f); - camera = Camera(toCoreVec3(cameraPosition), toCoreVec3(cameraTarget), projectionMatrix, 1.069f, 0.4f); - uiState.cameraMoveSpeed = camera.getMoveSpeed(); - uiState.cameraRotateSpeed = camera.getRotateSpeed(); + cameraProjection = projectionMatrix; + camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(cameraPosition.x, cameraPosition.y, cameraPosition.z), + hlsl::float64_t3(cameraTarget.x, cameraTarget.y, cameraTarget.z), + {1.069, 0.4}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; + uiState.cameraMoveSpeed = 1.069f; + uiState.cameraRotateSpeed = 0.4f; uiState.cameraControlApplied = !uiState.cameraControlEnabled; } diff --git a/50.IESViewer/AppRender.cpp b/50.IESViewer/AppRender.cpp index a06b2702a..a6960b736 100644 --- a/50.IESViewer/AppRender.cpp +++ b/50.IESViewer/AppRender.cpp @@ -82,7 +82,7 @@ bool IESViewer::recreate3DPlotFramebuffers(uint32_t width, uint32_t height) const float aspect = float(width) / float(height); const auto projectionMatrix = buildProjectionMatrixPerspectiveFovLH(hlsl::radians(uiState.cameraFovDeg), aspect, 0.1f, 10000.0f); - camera.setProjectionMatrix(projectionMatrix); + cameraProjection = projectionMatrix; return true; } @@ -119,8 +119,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo IESViewer::renderFrame(const std::chrono::mi uiState.cameraControlApplied = wantCameraControl; const float moveSpeed = wantCameraControl ? uiState.cameraMoveSpeed : 0.0f; const float rotateSpeed = wantCameraControl ? uiState.cameraRotateSpeed : 0.0f; - camera.setMoveSpeed(moveSpeed); - camera.setRotateSpeed(rotateSpeed); + CCameraSimpleFPSUtilities::applySpeedSettings(*camera, {moveSpeed, rotateSpeed}); } @@ -144,20 +143,16 @@ IQueue::SSubmitInfo::SSemaphoreInfo IESViewer::renderFrame(const std::chrono::mi std::vector mouse{}; std::vector keyboard{}; } captured; - camera.beginInputProcessing(nextPresentationTimestamp); if (windowFocused) { mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - if (wantCameraControl) - camera.mouseProcess(events); processMouse(events); for (const auto& e : events) captured.mouse.emplace_back(e); }, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { - camera.keyboardProcess(events); processKeyboard(events); for (const auto& e : events) captured.keyboard.emplace_back(e); @@ -168,29 +163,33 @@ IQueue::SSubmitInfo::SSemaphoreInfo IESViewer::renderFrame(const std::chrono::mi mouse.consumeEvents([&](const IMouseEventChannel::range_t&) -> void {}, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t&) -> void {}, m_logger.get()); } - camera.endInputProcessing(nextPresentationTimestamp); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents( + captured.mouse, + captured.keyboard, + nextPresentationTimestamp, + cameraInputRuntime, + cameraInputConfig); + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); { const float maxRadius = m_plotRadius * 0.98f; const float clampRadius = maxRadius * 0.999f; - using core_vec_t = std::remove_cv_t>; - const auto toHlslVec3 = [](const core_vec_t& v) - { - return float32_t3(v.x, v.y, v.z); - }; - const auto toCoreVec3 = [](const float32_t3& v) - { - return core_vec_t(v.x, v.y, v.z); - }; - auto pos = toHlslVec3(camera.getPosition()); + auto pos = hlsl::CCameraMathUtilities::castVector(camera->getGimbal().getPosition()); const float dist = length(pos); if (dist > maxRadius) { - const auto target = toHlslVec3(camera.getTarget()); + const auto target = hlsl::CCameraMathUtilities::castVector(camera->getGimbal().getWorldTarget()); const auto forward = target - pos; pos = normalize(pos) * clampRadius; - camera.setPosition(toCoreVec3(pos)); - camera.setTarget(toCoreVec3(pos + forward)); + auto clampedCamera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(pos.x, pos.y, pos.z), + hlsl::float64_t3((pos + forward).x, (pos + forward).y, (pos + forward).z), + { + uiState.cameraControlApplied ? uiState.cameraMoveSpeed : 0.0f, + uiState.cameraControlApplied ? uiState.cameraRotateSpeed : 0.0f}); + if (clampedCamera) + camera = std::move(clampedCamera); } } @@ -362,13 +361,8 @@ IQueue::SSubmitInfo::SSemaphoreInfo IESViewer::renderFrame(const std::chrono::mi cb->beginDebugMarker("IES::graphics 3D plot"); cb->beginRenderPass(info3D, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); { - float32_t3x4 viewMatrix; - float32_t4x4 viewProjMatrix; - // TODO: get rid of legacy matrices - { - viewMatrix = camera.getViewMatrix(); - viewProjMatrix = camera.getConcatenatedMatrix(); - } + const float32_t3x4 viewMatrix = hlsl::float32_t3x4(camera->getGimbal().getViewMatrix()); + const float32_t4x4 viewProjMatrix = hlsl::math::linalg::promoted_mul(cameraProjection, viewMatrix); const auto viewParams = CSimpleIESRenderer::SViewParams(viewMatrix, viewProjMatrix); const auto iesParams = CSimpleIESRenderer::SIESParams({ .radius = m_plotRadius, .ds = m_descriptors[0u].get(), .texID = static_cast(uiState.activeAssetIx), .mode = uiState.mode.sphere.value, .wireframe = uiState.wireframeEnabled }); diff --git a/50.IESViewer/AppUI.cpp b/50.IESViewer/AppUI.cpp index c376f7730..e5528d60a 100644 --- a/50.IESViewer/AppUI.cpp +++ b/50.IESViewer/AppUI.cpp @@ -95,7 +95,7 @@ void IESViewer::uiListener() return; const float aspect = float(m_plot3DWidth) / float(m_plot3DHeight); const auto projectionMatrix = buildProjectionMatrixPerspectiveFovLH(hlsl::radians(uiState.cameraFovDeg), aspect, 0.1f, 10000.0f); - camera.setProjectionMatrix(projectionMatrix); + cameraProjection = projectionMatrix; }; auto draw3DControls = [&]() @@ -196,8 +196,7 @@ void IESViewer::uiListener() if (speedChanged && uiState.cameraControlEnabled) { - camera.setMoveSpeed(uiState.cameraMoveSpeed); - camera.setRotateSpeed(uiState.cameraRotateSpeed); + CCameraSimpleFPSUtilities::applySpeedSettings(*camera, {uiState.cameraMoveSpeed, uiState.cameraRotateSpeed}); } if (fovChanged) @@ -534,7 +533,7 @@ void IESViewer::uiListener() const float ndcX = u * 2.0f - 1.0f; const float ndcY = v * 2.0f - 1.0f; - float32_t4x4 viewProj = camera.getConcatenatedMatrix(); + float32_t4x4 viewProj = hlsl::math::linalg::promoted_mul(cameraProjection, hlsl::float32_t3x4(camera->getGimbal().getViewMatrix())); const auto invViewProj = inverse(viewProj); const float32_t4 nearPoint(ndcX, ndcY, 0.0f, 1.0f); @@ -544,13 +543,7 @@ void IESViewer::uiListener() nearWorld /= nearWorld.w; farWorld /= farWorld.w; - using core_vec_t = std::remove_cv_t>; - const auto toHlslVec3 = [](const core_vec_t& v) - { - return float32_t3(v.x, v.y, v.z); - }; - - const float32_t3 origin = toHlslVec3(camera.getPosition()); + const float32_t3 origin = hlsl::CCameraMathUtilities::castVector(camera->getGimbal().getPosition()); const float32_t3 farPos = float32_t3(farWorld); float32_t3 direction = normalize(farPos - origin); diff --git a/54_Transformations/main.cpp b/54_Transformations/main.cpp index 960b82562..1b03af3bf 100644 --- a/54_Transformations/main.cpp +++ b/54_Transformations/main.cpp @@ -5,7 +5,6 @@ #define _NBL_STATIC_LIB_ #include -#include "CCamera.hpp" #include "../common/CommonAPI.h" using namespace nbl; @@ -968,4 +967,4 @@ class TransformationApp : public ApplicationBase core::matrix4SIMD viewProj; }; -NBL_COMMON_API_MAIN(TransformationApp) \ No newline at end of file +NBL_COMMON_API_MAIN(TransformationApp) diff --git a/61_UI/AppCameraConfigJsonParsing.cpp b/61_UI/AppCameraConfigJsonParsing.cpp new file mode 100644 index 000000000..b05411bbf --- /dev/null +++ b/61_UI/AppCameraConfigJsonParsing.cpp @@ -0,0 +1,678 @@ +#include "app/AppCameraConfigUtilities.hpp" + +#include +#include +#include +#include + +#include "keysmapping.hpp" +#include "nlohmann/json.hpp" +#include "nbl/ext/Cameras/CArcballCamera.hpp" +#include "nbl/ext/Cameras/CChaseCamera.hpp" +#include "nbl/ext/Cameras/CDollyCamera.hpp" +#include "nbl/ext/Cameras/CDollyZoomCamera.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CFreeLockCamera.hpp" +#include "nbl/ext/Cameras/CIsometricCamera.hpp" +#include "nbl/ext/Cameras/COrbitCamera.hpp" +#include "nbl/ext/Cameras/CPathCamera.hpp" +#include "nbl/ext/Cameras/CTopDownCamera.hpp" +#include "nbl/ext/Cameras/CTurntableCamera.hpp" + +namespace nbl::system +{ + +using camera_json_t = nlohmann::json; + +struct SCameraConfigJsonKeys final +{ + static constexpr std::string_view Type = "type"; + static constexpr std::string_view Position = "position"; + static constexpr std::string_view Orientation = "orientation"; + static constexpr std::string_view Target = "target"; + static constexpr std::string_view BaseFov = "baseFov"; + static constexpr std::string_view Cameras = "cameras"; + static constexpr std::string_view Projections = "projections"; + static constexpr std::string_view Bindings = "bindings"; + static constexpr std::string_view Keyboard = "keyboard"; + static constexpr std::string_view Mouse = "mouse"; + static constexpr std::string_view Mappings = "mappings"; + static constexpr std::string_view ScriptedInput = "scripted_input"; + static constexpr std::string_view Viewports = "viewports"; + static constexpr std::string_view Planars = "planars"; + static constexpr std::string_view Projection = "projection"; + static constexpr std::string_view Camera = "camera"; + static constexpr std::string_view ZNear = "zNear"; + static constexpr std::string_view ZFar = "zFar"; + static constexpr std::string_view Fov = "fov"; + static constexpr std::string_view OrthoWidth = "orthoWidth"; +}; + +struct SCameraConfigTypeNames final +{ + static constexpr std::string_view Fps = "FPS"; + static constexpr std::string_view Free = "Free"; + static constexpr std::string_view Orbit = "Orbit"; + static constexpr std::string_view Arcball = "Arcball"; + static constexpr std::string_view Turntable = "Turntable"; + static constexpr std::string_view TopDown = "TopDown"; + static constexpr std::string_view Isometric = "Isometric"; + static constexpr std::string_view Chase = "Chase"; + static constexpr std::string_view Dolly = "Dolly"; + static constexpr std::string_view PathRig = "PathRig"; + static constexpr std::string_view DollyZoom = "DollyZoom"; + static constexpr std::string_view PerspectiveProjection = "perspective"; + static constexpr std::string_view OrthographicProjection = "orthographic"; +}; + +template +bool tryCreateOrientationCameraFromSpec( + const camera_json_t& jCamera, + const double moveScale, + const double rotationScale, + std::string_view typeName, + std::string& error, + core::smart_refctd_ptr& outCamera); + +template +bool tryCreateTargetCameraFromSpec( + const camera_json_t& jCamera, + const double moveScale, + const double rotationScale, + std::string_view typeName, + std::string& error, + core::smart_refctd_ptr& outCamera); + +bool tryCreateDollyZoomCameraFromSpec( + const camera_json_t& jCamera, + const double moveScale, + const double rotationScale, + std::string_view typeName, + std::string& error, + core::smart_refctd_ptr& outCamera); + +struct SCameraConfigCameraFactorySpec final +{ + using create_t = bool (*)( + const camera_json_t&, + double, + double, + std::string_view, + std::string&, + core::smart_refctd_ptr&); + + std::string_view typeName = {}; + create_t create = nullptr; + double moveScale = SCameraAppCameraFactoryDefaults::DefaultMoveScale; + double rotationScale = SCameraAppCameraFactoryDefaults::DefaultRotateScale; +}; + +inline constexpr std::array CameraFactorySpecs = {{ + { SCameraConfigTypeNames::Fps, &tryCreateOrientationCameraFromSpec, SCameraAppCameraFactoryDefaults::DefaultMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Free, &tryCreateOrientationCameraFromSpec, SCameraAppCameraFactoryDefaults::DefaultMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Orbit, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Arcball, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Turntable, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::TopDown, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Isometric, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Chase, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::Dolly, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::PathRig, &tryCreateTargetCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale }, + { SCameraConfigTypeNames::DollyZoom, &tryCreateDollyZoomCameraFromSpec, SCameraAppCameraFactoryDefaults::TargetRigMoveScale, SCameraAppCameraFactoryDefaults::DefaultRotateScale } +}}; + +inline bool jsonContainsAll(const camera_json_t& json, std::initializer_list keys) +{ + for (const auto key : keys) + { + if (!json.contains(key)) + return false; + } + return true; +} + +template +inline std::array readJsonArray(const camera_json_t& json, const std::string_view key) +{ + return json[key].get>(); +} + +inline hlsl::float64_t3 readJsonFloat64Vec3(const camera_json_t& json, const std::string_view key) +{ + const auto value = readJsonArray(json, key); + return hlsl::float64_t3(value[0], value[1], value[2]); +} + +inline hlsl::camera_quaternion_t readJsonQuaternion(const camera_json_t& json, const std::string_view key) +{ + const auto value = readJsonArray(json, key); + return hlsl::CCameraMathUtilities::makeQuaternionFromComponents(value[0], value[1], value[2], value[3]); +} + +template +inline core::smart_refctd_ptr makeCameraAsBase(Args&&... args) +{ + return core::make_smart_refctd_ptr(std::forward(args)...); +} + +template +bool tryLoadBindingMapFromJson( + const camera_json_t& jBinding, + const char* bindingTypeLabel, + const char* bindingCodeLabel, + TResolveCode&& resolveCode, + TMap& outBinding, + std::string& error) +{ + using code_type = std::remove_cvref_t>; + outBinding.clear(); + if (!jBinding.contains(SCameraConfigJsonKeys::Mappings)) + { + error = std::string("Expected \"mappings\" keyword for ") + bindingTypeLabel + " binding definition."; + return false; + } + + for (const auto& [key, value] : jBinding[SCameraConfigJsonKeys::Mappings].items()) + { + const auto nativeCode = resolveCode(key.c_str()); + if (nativeCode == code_type{}) + { + error = std::string("Invalid native ") + bindingCodeLabel + " \"" + key + "\" code mapping for " + bindingTypeLabel + " binding."; + return false; + } + + outBinding[nativeCode] = core::CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + } + + return true; +} + +inline void initializeCameraMotionConfig(core::ICamera& camera, const double moveScale, const double rotationScale) +{ + camera.setMotionScales(moveScale, rotationScale); +} + +template +inline bool tryCreateOrientationCameraFromSpec( + const camera_json_t& jCamera, + const double moveScale, + const double rotationScale, + std::string_view typeName, + std::string& error, + core::smart_refctd_ptr& outCamera) +{ + if (!jsonContainsAll(jCamera, { SCameraConfigJsonKeys::Position, SCameraConfigJsonKeys::Orientation })) + { + error = std::string(typeName) + " camera requires \"position\" and \"orientation\"."; + return false; + } + + auto camera = makeCameraAsBase( + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Position), + readJsonQuaternion(jCamera, SCameraConfigJsonKeys::Orientation)); + initializeCameraMotionConfig(*camera, moveScale, rotationScale); + outCamera = std::move(camera); + return true; +} + +template +inline bool tryCreateTargetCameraFromSpec( + const camera_json_t& jCamera, + const double moveScale, + const double rotationScale, + std::string_view typeName, + std::string& error, + core::smart_refctd_ptr& outCamera) +{ + if (!jsonContainsAll(jCamera, { SCameraConfigJsonKeys::Position, SCameraConfigJsonKeys::Target })) + { + error = std::string(typeName) + " camera requires \"position\" and \"target\"."; + return false; + } + + auto camera = makeCameraAsBase( + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Position), + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Target)); + initializeCameraMotionConfig(*camera, moveScale, rotationScale); + outCamera = std::move(camera); + return true; +} + +inline bool tryCreateDollyZoomCameraFromSpec( + const camera_json_t& jCamera, + const double moveScale, + const double rotationScale, + std::string_view typeName, + std::string& error, + core::smart_refctd_ptr& outCamera) +{ + if (!jsonContainsAll(jCamera, { SCameraConfigJsonKeys::Position, SCameraConfigJsonKeys::Target })) + { + error = std::string(typeName) + " camera requires \"position\" and \"target\"."; + return false; + } + + auto camera = + jCamera.contains(SCameraConfigJsonKeys::BaseFov) ? + makeCameraAsBase( + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Position), + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Target), + jCamera[SCameraConfigJsonKeys::BaseFov].get()) : + makeCameraAsBase( + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Position), + readJsonFloat64Vec3(jCamera, SCameraConfigJsonKeys::Target)); + + initializeCameraMotionConfig(*camera, moveScale, rotationScale); + outCamera = std::move(camera); + return true; +} + +inline const SCameraConfigCameraFactorySpec* findCameraFactorySpec(const std::string_view typeName) +{ + for (const auto& spec : CameraFactorySpecs) + { + if (spec.typeName == typeName) + return &spec; + } + return nullptr; +} + +inline bool tryParseViewportBindingSelectionFromJson( + const camera_json_t& json, + const char* label, + SCameraViewportBindingSelection& outSelection, + std::string& error) +{ + outSelection = {}; + if (!json.is_object()) + { + error = std::string("Expected object for ") + label + "."; + return false; + } + + const auto tryParseIx = [&](const std::string_view key, std::optional& outIx) -> bool + { + if (!json.contains(key)) + return true; + if (!json[key].is_number_unsigned()) + { + error = std::string("Expected unsigned integer for \"") + std::string(key) + "\" in " + label + "."; + return false; + } + outIx = json[key].get(); + return true; + }; + + return tryParseIx(SCameraConfigJsonKeys::Keyboard, outSelection.keyboard) && + tryParseIx(SCameraConfigJsonKeys::Mouse, outSelection.mouse); +} + +inline bool tryParseViewportConfigFromJson( + const camera_json_t& json, + SCameraViewportConfig& outConfig, + std::string& error) +{ + outConfig = {}; + if (!jsonContainsAll(json, { SCameraConfigJsonKeys::Projection, SCameraConfigJsonKeys::Bindings })) + { + error = "\"projection\" or \"bindings\" missing in viewport object."; + return false; + } + if (!json[SCameraConfigJsonKeys::Projection].is_number_unsigned()) + { + error = "Expected unsigned integer for viewport projection index."; + return false; + } + + outConfig.projectionIx = json[SCameraConfigJsonKeys::Projection].get(); + return tryParseViewportBindingSelectionFromJson( + json[SCameraConfigJsonKeys::Bindings], + "viewport bindings", + outConfig.bindings, + error); +} + +inline bool tryParsePlanarConfigFromJson( + const camera_json_t& json, + SCameraPlanarConfig& outConfig, + std::string& error) +{ + outConfig = {}; + if (!jsonContainsAll(json, { SCameraConfigJsonKeys::Camera, SCameraConfigJsonKeys::Viewports })) + { + error = "Expected \"camera\" value and \"viewports\" list in planar object."; + return false; + } + if (!json[SCameraConfigJsonKeys::Camera].is_number_unsigned()) + { + error = "Expected unsigned integer camera index in planar object."; + return false; + } + if (!json[SCameraConfigJsonKeys::Viewports].is_array()) + { + error = "Expected array for planar viewport indices."; + return false; + } + + outConfig.cameraIx = json[SCameraConfigJsonKeys::Camera].get(); + outConfig.viewportIxs = json[SCameraConfigJsonKeys::Viewports].get>(); + return true; +} + +template +bool tryLoadBindingCollectionFromJson( + const camera_json_t& root, + const std::string_view key, + const char* bindingTypeLabel, + const char* bindingCodeLabel, + ResolveCode&& resolveCode, + Collection& outCollection, + std::string& error) +{ + outCollection.clear(); + if (!root.contains(key)) + { + error = std::string("Expected \"") + std::string(key) + "\" keyword in bindings definition."; + return false; + } + if (!root[key].is_array()) + { + error = std::string("\"") + std::string(key) + "\" bindings must be an array."; + return false; + } + + outCollection.reserve(root[key].size()); + for (const auto& bindingJson : root[key]) + { + auto& binding = outCollection.emplace_back(); + if (!tryLoadBindingMapFromJson( + bindingJson, + bindingTypeLabel, + bindingCodeLabel, + std::forward(resolveCode), + binding, + error)) + { + return false; + } + } + + return true; +} + +inline bool tryAppendProjectionFromJson( + const camera_json_t& jProjection, + std::vector& outProjections, + std::string& error) +{ + if (!jsonContainsAll(jProjection, { + SCameraConfigJsonKeys::Type, + SCameraConfigJsonKeys::ZNear, + SCameraConfigJsonKeys::ZFar })) + { + error = "Projection entry requires \"type\", \"zNear\", and \"zFar\"."; + return false; + } + + const float zNear = jProjection[SCameraConfigJsonKeys::ZNear].get(); + const float zFar = jProjection[SCameraConfigJsonKeys::ZFar].get(); + const auto type = jProjection[SCameraConfigJsonKeys::Type].get(); + + if (type == SCameraConfigTypeNames::PerspectiveProjection) + { + if (!jProjection.contains(SCameraConfigJsonKeys::Fov)) + { + error = "Perspective projection requires \"fov\"."; + return false; + } + + outProjections.emplace_back( + core::IPlanarProjection::CProjection::create( + zNear, + zFar, + jProjection[SCameraConfigJsonKeys::Fov].get())); + return true; + } + + if (type == SCameraConfigTypeNames::OrthographicProjection) + { + if (!jProjection.contains(SCameraConfigJsonKeys::OrthoWidth)) + { + error = "Orthographic projection requires \"orthoWidth\"."; + return false; + } + + outProjections.emplace_back( + core::IPlanarProjection::CProjection::create( + zNear, + zFar, + jProjection[SCameraConfigJsonKeys::OrthoWidth].get())); + return true; + } + + error = "Unsupported projection type \"" + type + "\"."; + return false; +} + +inline bool tryCreateCameraFromJson( + const camera_json_t& jCamera, + std::string& error, + core::smart_refctd_ptr& outCamera) +{ + if (!jCamera.contains(SCameraConfigJsonKeys::Type)) + { + error = "Camera entry missing \"type\"."; + return false; + } + + if (!jCamera.contains(SCameraConfigJsonKeys::Position)) + { + error = "Camera entry missing \"position\"."; + return false; + } + + const auto type = jCamera[SCameraConfigJsonKeys::Type].get(); + const auto* spec = findCameraFactorySpec(type); + if (!spec || !spec->create) + { + error = "Unsupported camera type \"" + type + "\"."; + return false; + } + + return spec->create( + jCamera, + spec->moveScale, + spec->rotationScale, + spec->typeName, + error, + outCamera); +} + +inline bool tryParseCameraConfigJsonText( + const std::string_view text, + camera_json_t& outJson, + std::string* const error) +{ + try + { + outJson = camera_json_t::parse(text); + return true; + } + catch (const std::exception& e) + { + if (error) + *error = "JSON parse error: " + std::string(e.what()); + return false; + } +} + +bool tryLoadCameraCollectionFromJson( + const camera_json_t& json, + std::string& error, + std::vector>& outCameras) +{ + outCameras.clear(); + if (!json.contains(SCameraConfigJsonKeys::Cameras) || !json[SCameraConfigJsonKeys::Cameras].is_array()) + { + error = "Missing \"cameras\" array in config."; + return false; + } + + outCameras.reserve(json[SCameraConfigJsonKeys::Cameras].size()); + for (const auto& jCamera : json[SCameraConfigJsonKeys::Cameras]) + { + core::smart_refctd_ptr camera; + if (!tryCreateCameraFromJson(jCamera, error, camera)) + return false; + outCameras.emplace_back(std::move(camera)); + } + + if (outCameras.empty()) + { + error = "No cameras defined."; + return false; + } + + return true; +} + +bool tryLoadProjectionCollectionFromJson( + const camera_json_t& json, + std::string& error, + std::vector& outProjections) +{ + outProjections.clear(); + if (!json.contains(SCameraConfigJsonKeys::Projections) || !json[SCameraConfigJsonKeys::Projections].is_array()) + { + error = "Missing \"projections\" array in config."; + return false; + } + + outProjections.reserve(json[SCameraConfigJsonKeys::Projections].size()); + for (const auto& jProjection : json[SCameraConfigJsonKeys::Projections]) + { + if (!tryAppendProjectionFromJson(jProjection, outProjections, error)) + return false; + } + + return true; +} + +bool tryLoadInputBindingCollectionsFromJson( + const camera_json_t& json, + std::string& error, + SCameraInputBindingCollections& outBindings) +{ + outBindings = {}; + if (!json.contains(SCameraConfigJsonKeys::Bindings)) + { + error = "Expected \"bindings\" keyword in camera JSON."; + return false; + } + + const auto& jBindings = json[SCameraConfigJsonKeys::Bindings]; + if (!tryLoadBindingCollectionFromJson( + jBindings, + SCameraConfigJsonKeys::Keyboard, + "keyboard", + "key", + [](const char* key) { return stringToKeyCode(key); }, + outBindings.keyboard, + error)) + { + return false; + } + + if (!tryLoadBindingCollectionFromJson( + jBindings, + SCameraConfigJsonKeys::Mouse, + "mouse", + "key", + [](const char* key) { return stringToMouseCode(key); }, + outBindings.mouse, + error)) + { + return false; + } + + return true; +} + +bool tryLoadPlanarConfigCollectionsFromJson( + const camera_json_t& json, + std::string& error, + SCameraPlanarConfigCollections& outPlanarConfig) +{ + outPlanarConfig = {}; + if (!(json.contains(SCameraConfigJsonKeys::Viewports) && json.contains(SCameraConfigJsonKeys::Planars))) + { + error = "Expected \"viewports\" and \"planars\" lists in JSON."; + return false; + } + if (!json[SCameraConfigJsonKeys::Viewports].is_array() || !json[SCameraConfigJsonKeys::Planars].is_array()) + { + error = "\"viewports\" and \"planars\" must be arrays."; + return false; + } + + outPlanarConfig.viewports.reserve(json[SCameraConfigJsonKeys::Viewports].size()); + for (const auto& jViewport : json[SCameraConfigJsonKeys::Viewports]) + { + auto& viewportConfig = outPlanarConfig.viewports.emplace_back(); + if (!tryParseViewportConfigFromJson(jViewport, viewportConfig, error)) + return false; + } + + outPlanarConfig.planars.reserve(json[SCameraConfigJsonKeys::Planars].size()); + for (const auto& jPlanar : json[SCameraConfigJsonKeys::Planars]) + { + auto& planarConfig = outPlanarConfig.planars.emplace_back(); + if (!tryParsePlanarConfigFromJson(jPlanar, planarConfig, error)) + return false; + } + + if (!outPlanarConfig.valid()) + { + error = "No planars defined."; + return false; + } + return true; +} + +bool tryBuildCameraConfigCollections( + const camera_json_t& json, + SCameraConfigCollections& outCollections, + std::string& error) +{ + outCollections = {}; + if (json.contains(SCameraConfigJsonKeys::ScriptedInput)) + outCollections.embeddedScriptedInputText = json[SCameraConfigJsonKeys::ScriptedInput].dump(); + + if (!tryLoadCameraCollectionFromJson(json, error, outCollections.cameras)) + return false; + + if (!tryLoadProjectionCollectionFromJson(json, error, outCollections.projections)) + return false; + + if (!tryLoadInputBindingCollectionsFromJson(json, error, outCollections.bindings)) + return false; + + if (!tryLoadPlanarConfigCollectionsFromJson(json, error, outCollections.planarConfig)) + return false; + + return true; +} + +bool tryBuildCameraConfigCollections( + const std::string_view text, + SCameraConfigCollections& outCollections, + std::string& error) +{ + camera_json_t json = {}; + if (!tryParseCameraConfigJsonText(text, json, &error)) + return false; + + return tryBuildCameraConfigCollections(json, outCollections, error); +} + +} // namespace nbl::system diff --git a/61_UI/AppCameraConfigUtilities.cpp b/61_UI/AppCameraConfigUtilities.cpp new file mode 100644 index 000000000..6bd999f23 --- /dev/null +++ b/61_UI/AppCameraConfigUtilities.cpp @@ -0,0 +1,65 @@ +#include "app/AppCameraConfigUtilities.hpp" + +namespace nbl::system +{ + +bool tryLoadCameraConfigCollections( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraConfigLoadResult& outLoadResult, + SCameraConfigCollections& outCollections, + std::string* const error) +{ + outLoadResult = {}; + outCollections = {}; + + std::string loadOrParseError; + if (!tryLoadCameraConfigText(context, request, outLoadResult, &loadOrParseError)) + { + if (error) + *error = loadOrParseError; + return false; + } + + if (!tryBuildCameraConfigCollections(std::string_view(outLoadResult.text), outCollections, loadOrParseError)) + { + if (error) + *error = loadOrParseError; + return false; + } + + return true; +} + +bool tryBuildCameraPlanarRuntimeBootstrap( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraPlanarRuntimeBootstrap& outBootstrap, + std::string* const error) +{ + outBootstrap = {}; + + std::string runtimeError; + if (!tryLoadCameraConfigCollections( + context, + request, + outBootstrap.loadResult, + outBootstrap.collections, + &runtimeError)) + { + if (error) + *error = runtimeError; + return false; + } + + if (!tryBuildCameraPlanarRuntime(outBootstrap.collections, outBootstrap.planars, runtimeError)) + { + if (error) + *error = runtimeError; + return false; + } + + return true; +} + +} // namespace nbl::system diff --git a/61_UI/AppCameraConfiguration.cpp b/61_UI/AppCameraConfiguration.cpp new file mode 100644 index 000000000..788c8962a --- /dev/null +++ b/61_UI/AppCameraConfiguration.cpp @@ -0,0 +1,125 @@ +#include "app/App.hpp" + +#include +#include +#include +#include + +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" +#include "app/AppViewportBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPersistence.hpp" + +bool App::initializeCameraConfiguration(const argparse::ArgumentParser& program) +{ + nbl::system::SCameraPlanarRuntimeBootstrap runtimeBootstrap = {}; + std::optional pendingScriptedSequence; + if (!tryBuildCameraConfigurationBootstrap(program, runtimeBootstrap, pendingScriptedSequence)) + return false; + + return initializePlanarRuntimeState(runtimeBootstrap, pendingScriptedSequence); +} + +bool App::tryBuildCameraConfigurationBootstrap( + const argparse::ArgumentParser& program, + nbl::system::SCameraPlanarRuntimeBootstrap& outRuntimeBootstrap, + std::optional& outPendingScriptedSequence) +{ + const std::optional cameraJsonFile = + program.is_used("--file") ? + std::optional(program.get("--file")) : + std::optional(std::nullopt); + + std::string jsonError; + if (!nbl::system::tryBuildCameraPlanarRuntimeBootstrap( + getCameraAppResourceContext(), + { + .requestedPath = cameraJsonFile, + .fallbackToDefault = true + }, + outRuntimeBootstrap, + &jsonError)) + return logFail("%s", jsonError.c_str()); + auto& cameraConfig = outRuntimeBootstrap.loadResult; + auto& cameraCollections = outRuntimeBootstrap.collections; + + const bool hasUserConfig = cameraJsonFile.has_value(); + if (cameraConfig.usedDefaultConfig()) + { + if (hasUserConfig) + m_logger->log("Cannot open input \"%s\" json file (%s). Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().string().c_str(), cameraConfig.requestedPathError.c_str()); + else + m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); + } + + outPendingScriptedSequence.reset(); + if (!tryLoadConfiguredScriptedInput(program, cameraCollections, outPendingScriptedSequence)) + return false; + return true; +} + +bool App::initializePlanarRuntimeState( + const nbl::system::SCameraPlanarRuntimeBootstrap& runtimeBootstrap, + const std::optional& pendingScriptedSequence) +{ + m_planarProjections = runtimeBootstrap.planars; + + if (!nbl::ui::initializeWindowBindingDefaults( + getPlanarProjectionSpan(), + std::span(m_viewports.windowBindings.data(), m_viewports.windowBindings.size()))) + { + return logFail("Failed to initialize default viewport bindings."); + } + + std::string cameraConfigError; + if (!nbl::system::tryCaptureInitialPlanarPresets( + m_cameraGoalSolver, + getPlanarProjectionSpan(), + m_presetAuthoring.initialPlanarPresets, + cameraConfigError)) + { + return logFail("%s", cameraConfigError.c_str()); + } + + initializePlanarFollowConfigs(); + bindManipulatedModel(); + + if (pendingScriptedSequence.has_value() && !expandPendingScriptedSequence(*pendingScriptedSequence)) + return false; + + return true; +} + +SCameraFollowConfig App::makeExampleDefaultFollowConfig(const ICamera* const camera) const +{ + auto config = nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(camera); + if (!camera) + return config; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Free: + config.enabled = true; + config.mode = ECameraFollowMode::LookAtTarget; + break; + default: + break; + } + + return config; +} + +void App::initializePlanarFollowConfigs() +{ + resetFollowTargetToDefault(); + m_sceneInteraction.planarFollowConfigs.clear(); + m_sceneInteraction.planarFollowConfigs.reserve(m_planarProjections.size()); + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) + { + auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + auto config = makeExampleDefaultFollowConfig(camera); + m_sceneInteraction.planarFollowConfigs.emplace_back(config); + if (config.enabled) + captureFollowOffsetsForPlanar(planarIx); + } +} diff --git a/61_UI/AppCameraPlanarRuntime.cpp b/61_UI/AppCameraPlanarRuntime.cpp new file mode 100644 index 000000000..16a3dc4fd --- /dev/null +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -0,0 +1,184 @@ +#include "app/AppCameraConfigUtilities.hpp" + +#include +#include + +template +bool tryApplyProjectionBindingSelection( + const std::optional& bindingIx, + std::span bindings, + const char* label, + ApplyBinding&& applyBinding, + std::string& error) +{ + if (!bindingIx.has_value()) + return true; + if (bindingIx.value() >= bindings.size()) + { + error = std::string(label) + " binding index out of range."; + return false; + } + + applyBinding(bindings[bindingIx.value()]); + return true; +} + +namespace nbl::system +{ + +bool tryCaptureInitialPlanarPresets( + const core::CCameraGoalSolver& goalSolver, + std::span> planars, + std::vector& outPresets, + std::string& outError) +{ + outPresets.clear(); + outPresets.reserve(planars.size()); + for (uint32_t planarIx = 0u; planarIx < planars.size(); ++planarIx) + { + auto* camera = planars[planarIx] ? planars[planarIx]->getCamera() : nullptr; + const std::string presetName = "Planar " + std::to_string(planarIx); + const auto captureAnalysis = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(goalSolver, camera); + if (!captureAnalysis.canCapture) + { + const auto kindLabel = camera ? std::string(core::CCameraKindUtilities::getCameraKindLabel(camera->getKind())) : std::string("Unknown"); + const auto reason = + !captureAnalysis.hasCamera ? "missing camera" : + (!captureAnalysis.capturedGoal ? "capture failed" : + (!captureAnalysis.finiteGoal ? "non-finite goal" : "unknown")); + std::string goalDetails; + if (!captureAnalysis.finiteGoal) + { + const auto& goal = captureAnalysis.goal; + goalDetails = + " position=(" + std::to_string(goal.position.x) + "," + std::to_string(goal.position.y) + "," + std::to_string(goal.position.z) + ")" + + " orientation=(" + std::to_string(goal.orientation.data.x) + "," + std::to_string(goal.orientation.data.y) + "," + std::to_string(goal.orientation.data.z) + "," + std::to_string(goal.orientation.data.w) + ")" + + " hasTarget=" + std::to_string(goal.hasTargetPosition) + + " target=(" + std::to_string(goal.targetPosition.x) + "," + std::to_string(goal.targetPosition.y) + "," + std::to_string(goal.targetPosition.z) + ")" + + " hasDistance=" + std::to_string(goal.hasDistance) + + " distance=" + std::to_string(goal.distance) + + " hasOrbit=" + std::to_string(goal.hasOrbitState) + + " orbit=(" + std::to_string(goal.orbitUv.x) + "," + std::to_string(goal.orbitUv.y) + "," + std::to_string(goal.orbitDistance) + ")"; + } + outError = + "Failed to capture initial planar preset " + std::to_string(planarIx) + + " for camera kind \"" + kindLabel + "\": " + reason + goalDetails; + return false; + } + + core::CCameraPreset preset = {}; + if (!core::CCameraPresetFlowUtilities::tryCapturePreset(captureAnalysis, camera, presetName, preset)) + { + outError = + "Failed to build initial planar preset " + std::to_string(planarIx) + + " for camera kind \"" + (camera ? std::string(core::CCameraKindUtilities::getCameraKindLabel(camera->getKind())) : std::string("Unknown")) + "\"."; + return false; + } + + outPresets.emplace_back(std::move(preset)); + } + + return true; +} + +bool tryBuildPlanarProjectionCollectionFromConfig( + const SCameraPlanarConfigCollections& planarConfig, + const std::span> cameras, + const std::span projections, + const SCameraInputBindingCollections& bindings, + std::vector>& outPlanars, + std::string& error) +{ + outPlanars.clear(); + if (!planarConfig.valid()) + { + error = "Camera planar config is missing."; + return false; + } + + outPlanars.reserve(planarConfig.planars.size()); + for (const auto& planarConfigEntry : planarConfig.planars) + { + const auto cameraIx = planarConfigEntry.cameraIx; + if (cameraIx >= cameras.size()) + { + error = "Planar camera index out of range."; + return false; + } + + auto& planar = outPlanars.emplace_back() = planar_projection_t::create(core::smart_refctd_ptr(cameras[cameraIx])); + for (const auto viewportIx : planarConfigEntry.viewportIxs) + { + if (viewportIx >= planarConfig.viewports.size()) + { + error = "Viewport index out of range in planar definition."; + return false; + } + + const auto& viewport = planarConfig.viewports[viewportIx]; + const auto projectionIx = viewport.projectionIx; + if (projectionIx >= projections.size()) + { + error = "Planar projection index out of range."; + return false; + } + + auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); + auto& projectionBinding = projection.getInputBinding(); + if (!tryApplyProjectionBindingSelection( + viewport.bindings.keyboard, + std::span(bindings.keyboard.data(), bindings.keyboard.size()), + "Keyboard", + [&](const auto& map) { projectionBinding.updateKeyboardMapping([&](auto& dst) { dst = map; }); }, + error)) + { + return false; + } + + if (!tryApplyProjectionBindingSelection( + viewport.bindings.mouse, + std::span(bindings.mouse.data(), bindings.mouse.size()), + "Mouse", + [&](const auto& map) { projectionBinding.updateMouseMapping([&](auto& dst) { dst = map; }); }, + error)) + { + return false; + } + } + } + + return !outPlanars.empty(); +} + +bool tryBuildCameraPlanarRuntime( + const SCameraConfigCollections& collections, + std::vector>& outPlanars, + std::string& error) +{ + if (!collections.planarConfig.valid()) + { + error = "Camera planar configuration is missing."; + return false; + } + + return tryBuildPlanarProjectionCollectionFromConfig( + collections.planarConfig, + std::span>(collections.cameras.data(), collections.cameras.size()), + std::span(collections.projections.data(), collections.projections.size()), + collections.bindings, + outPlanars, + error); +} + +bool tryGetEmbeddedCameraScriptedInputText( + const SCameraConfigCollections& collections, + std::string& outText) +{ + if (!collections.hasEmbeddedScriptedInputText()) + return false; + + outText = collections.embeddedScriptedInputText; + return true; +} + +} // namespace nbl::system diff --git a/61_UI/AppCameraUiState.cpp b/61_UI/AppCameraUiState.cpp new file mode 100644 index 000000000..41162624e --- /dev/null +++ b/61_UI/AppCameraUiState.cpp @@ -0,0 +1,101 @@ +#include "app/App.hpp" +#include "app/AppResourceUtilities.hpp" + +template +inline bool tryBindActiveViewportContext(TContext& outContext, const SActiveViewportRuntimeState& viewportState) +{ + outContext = {}; + outContext.viewport = viewportState; + return outContext.valid(); +} + +inline SCameraFollowConfig* tryGetViewportFollowConfig( + std::span followConfigs, + const SActiveViewportRuntimeState& viewportState) +{ + if (!viewportState.valid()) + return nullptr; + + const auto planarIx = viewportState.requireBinding().activePlanarIx; + if (planarIx >= followConfigs.size()) + return nullptr; + + return &followConfigs[planarIx]; +} + +nbl::system::SCameraAppResourceContext App::getCameraAppResourceContext() const +{ + return m_system ? + nbl::system::makeCameraAppResourceContext(*m_system, localInputCWD) : + nbl::system::SCameraAppResourceContext{}; +} + +ICamera* App::getActiveCamera() +{ + const auto viewportState = tryGetActiveViewportRuntimeState(); + return viewportState.camera; +} + +uint32_t App::getActivePlanarIx() const +{ + return m_viewports.activeRenderWindowIx < m_viewports.windowBindings.size() ? + m_viewports.windowBindings[m_viewports.activeRenderWindowIx].activePlanarIx : + SWindowControlBinding::InvalidPlanarIx; +} + +SCameraFollowConfig* App::getActiveFollowConfig() +{ + return tryGetViewportFollowConfig( + std::span(m_sceneInteraction.planarFollowConfigs.data(), m_sceneInteraction.planarFollowConfigs.size()), + tryGetActiveViewportRuntimeState()); +} + +const SCameraFollowConfig* App::getActiveFollowConfig() const +{ + const auto planarIx = getActivePlanarIx(); + if (planarIx >= m_sceneInteraction.planarFollowConfigs.size()) + return nullptr; + return &m_sceneInteraction.planarFollowConfigs[planarIx]; +} + +SActiveViewportRuntimeState App::tryGetActiveViewportRuntimeState() +{ + SActiveViewportRuntimeState viewportState = {}; + nbl::ui::tryBuildActiveViewportRuntimeState( + getPlanarProjectionSpan(), + std::span(m_viewports.windowBindings.data(), m_viewports.windowBindings.size()), + m_viewports.activeRenderWindowIx, + viewportState); + return viewportState; +} + +bool App::tryBuildActiveCameraInputContext(SActiveCameraInputContext& outContext) +{ + return tryBindActiveViewportContext(outContext, tryGetActiveViewportRuntimeState()); +} + +bool App::tryBuildActiveProjectionTabContext(SActiveProjectionTabContext& outContext) +{ + if (!tryBindActiveViewportContext(outContext, tryGetActiveViewportRuntimeState())) + return false; + + outContext.activeRenderWindowIxString = std::to_string(m_viewports.activeRenderWindowIx); + outContext.activePlanarIxString = std::to_string(outContext.requireBinding().activePlanarIx); + return true; +} + +bool App::tryBuildActiveScriptedCameraContext(SActiveScriptedCameraContext& outContext) +{ + if (!tryBindActiveViewportContext(outContext, tryGetActiveViewportRuntimeState())) + return false; + + outContext.followConfig = tryGetViewportFollowConfig( + std::span(m_sceneInteraction.planarFollowConfigs.data(), m_sceneInteraction.planarFollowConfigs.size()), + outContext.viewport); + const auto planarSpan = getPlanarProjectionSpan(); + outContext.hasProjectionContext = nbl::ui::tryBuildBindingProjectionContext( + planarSpan, + outContext.requireBinding(), + outContext.projectionContext); + return true; +} diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp new file mode 100644 index 000000000..0b6e559ae --- /dev/null +++ b/61_UI/AppControlPanel.cpp @@ -0,0 +1,61 @@ +#include "app/App.hpp" +#include "nbl/ext/Cameras/CCameraPersistence.hpp" + +bool App::savePresetsToFile(const nbl::system::path& path) +{ + return nbl::system::CCameraPersistenceUtilities::savePresetCollectionToFile( + *m_system, + path, + std::span(m_presetAuthoring.presets.data(), m_presetAuthoring.presets.size())); +} + +bool App::loadPresetsFromFile(const nbl::system::path& path) +{ + return nbl::system::CCameraPersistenceUtilities::loadPresetCollectionFromFile(*m_system, path, m_presetAuthoring.presets); +} + +bool App::saveKeyframesToFile(const nbl::system::path& path) +{ + return nbl::system::CCameraKeyframeTrackPersistenceUtilities::saveKeyframeTrackToFile(*m_system, path, m_playbackAuthoring.keyframeTrack); +} + +bool App::loadKeyframesFromFile(const nbl::system::path& path) +{ + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::loadKeyframeTrackFromFile(*m_system, path, m_playbackAuthoring.keyframeTrack)) + return false; + + clampPlaybackTimeToKeyframes(); + if (m_playbackAuthoring.keyframeTrack.keyframes.empty()) + clearApplyStatusBanner(m_playbackAuthoring.applyBanner); + return true; +} + +void App::DrawControlPanel() +{ + const nbl::ui::SCameraControlPanelStyle panelStyle = {}; + const ImVec2 displaySize = ImGui::GetIO().DisplaySize; + const ImVec2 panelSize = nbl::ui::CCameraControlPanelUiUtilities::calcControlPanelWindowSize(displaySize, panelStyle); + const ImVec2 panelPos = { 0.0f, 0.0f }; + ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); + + nbl::ui::CCameraControlPanelUiUtilities::pushControlPanelWindowStyle(panelStyle); + ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.0f); + if (m_cliRuntime.ciMode) + ImGui::SetNextWindowFocus(); + ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); + + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + nbl::ui::CCameraControlPanelUiUtilities::drawControlPanelWindowBackdrop(*drawList, ImGui::GetWindowPos(), ImGui::GetWindowSize(), panelStyle); + + drawControlPanelHeader(panelStyle); + ImGui::Spacing(); + drawControlPanelToggles(panelStyle); + ImGui::Separator(); + drawControlPanelTabs(panelStyle); + + ImGui::End(); + nbl::ui::CCameraControlPanelUiUtilities::popControlPanelWindowStyle(); +} + diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp new file mode 100644 index 000000000..a6b6edf15 --- /dev/null +++ b/61_UI/AppControlPanelCameraTab.cpp @@ -0,0 +1,157 @@ +#include "app/App.hpp" + +void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + using checkbox_spec_t = nbl::ui::SCameraControlPanelCheckboxSpec; + using slider_spec_t = nbl::ui::SCameraControlPanelSliderSpec; + + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("CameraPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("CameraInputHeader", "Input", panelStyle.AccentColor, panelStyle); + for (const auto& spec : { + checkbox_spec_t{ .label = "Mirror input to all cameras", .value = &m_cameraControls.mirrorInput, .hint = "Apply keyboard and mouse input to every camera" }, + checkbox_spec_t{ .label = "World translate", .value = &m_cameraControls.worldTranslate, .hint = "Translate in world space instead of camera space" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint(spec); + } + for (const auto& spec : { + slider_spec_t{ .label = "Keyboard scale", .value = &m_cameraControls.keyboardScale, .minValue = SCameraAppControlPanelRangeDefaults::InputScaleMin, .maxValue = SCameraAppControlPanelRangeDefaults::InputScaleMax, .format = "%.2f", .hint = "Scale keyboard movement magnitudes" }, + slider_spec_t{ .label = "Mouse move scale", .value = &m_cameraControls.mouseMoveScale, .minValue = SCameraAppControlPanelRangeDefaults::InputScaleMin, .maxValue = SCameraAppControlPanelRangeDefaults::InputScaleMax, .format = "%.2f", .hint = "Scale mouse move magnitudes" }, + slider_spec_t{ .label = "Mouse scroll scale", .value = &m_cameraControls.mouseScrollScale, .minValue = SCameraAppControlPanelRangeDefaults::InputScaleMin, .maxValue = SCameraAppControlPanelRangeDefaults::InputScaleMax, .format = "%.2f", .hint = "Scale mouse wheel magnitudes" }, + slider_spec_t{ .label = "Translate scale", .value = &m_cameraControls.translationScale, .minValue = SCameraAppControlPanelRangeDefaults::InputScaleMin, .maxValue = SCameraAppControlPanelRangeDefaults::InputScaleMax, .format = "%.2f", .hint = "Overall translation scale for virtual events" }, + slider_spec_t{ .label = "Rotate scale", .value = &m_cameraControls.rotationScale, .minValue = SCameraAppControlPanelRangeDefaults::InputScaleMin, .maxValue = SCameraAppControlPanelRangeDefaults::InputScaleMax, .format = "%.2f", .hint = "Overall rotation scale for virtual events" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("CameraConstraintsHeader", "Constraints", panelStyle.AccentColor, panelStyle); + for (const auto& spec : { + checkbox_spec_t{ .label = "Enable constraints", .value = &m_cameraConstraints.enabled, .hint = "Enable or disable all camera constraints" }, + checkbox_spec_t{ .label = "Clamp distance", .value = &m_cameraConstraints.clampDistance, .hint = "Clamp orbit distance to min/max" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint(spec); + } + for (const auto& spec : { + slider_spec_t{ .label = "Min distance", .value = &m_cameraConstraints.minDistance, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintDistanceMin, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintMinDistanceMax, .format = "%.3f", .flags = ImGuiSliderFlags_Logarithmic, .hint = "Minimum orbit distance" }, + slider_spec_t{ .label = "Max distance", .value = &m_cameraConstraints.maxDistance, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintDistanceMin, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintMaxDistanceMax, .format = "%.3f", .flags = ImGuiSliderFlags_Logarithmic, .hint = "Maximum orbit distance" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); + } + ImGui::Separator(); + for (const auto& spec : { + checkbox_spec_t{ .label = "Clamp pitch", .value = &m_cameraConstraints.clampPitch, .hint = "Clamp pitch angle" }, + checkbox_spec_t{ .label = "Clamp yaw", .value = &m_cameraConstraints.clampYaw, .hint = "Clamp yaw angle" }, + checkbox_spec_t{ .label = "Clamp roll", .value = &m_cameraConstraints.clampRoll, .hint = "Clamp roll angle" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint(spec); + } + for (const auto& spec : { + slider_spec_t{ .label = "Pitch min", .value = &m_cameraConstraints.pitchMinDeg, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMinDeg, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMaxDeg, .format = "%.1f", .hint = "Minimum pitch in degrees" }, + slider_spec_t{ .label = "Pitch max", .value = &m_cameraConstraints.pitchMaxDeg, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMinDeg, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMaxDeg, .format = "%.1f", .hint = "Maximum pitch in degrees" }, + slider_spec_t{ .label = "Yaw min", .value = &m_cameraConstraints.yawMinDeg, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMinDeg, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMaxDeg, .format = "%.1f", .hint = "Minimum yaw in degrees" }, + slider_spec_t{ .label = "Yaw max", .value = &m_cameraConstraints.yawMaxDeg, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMinDeg, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMaxDeg, .format = "%.1f", .hint = "Maximum yaw in degrees" }, + slider_spec_t{ .label = "Roll min", .value = &m_cameraConstraints.rollMinDeg, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMinDeg, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMaxDeg, .format = "%.1f", .hint = "Minimum roll in degrees" }, + slider_spec_t{ .label = "Roll max", .value = &m_cameraConstraints.rollMaxDeg, .minValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMinDeg, .maxValue = SCameraAppControlPanelRangeDefaults::ConstraintAngleMaxDeg, .format = "%.1f", .hint = "Maximum roll in degrees" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("OrbitHeader", "Orbit Target", panelStyle.AccentColor, panelStyle); + auto* activeCamera = getActiveCamera(); + ICamera::SphericalTargetState orbitState; + const bool hasOrbitTarget = activeCamera && activeCamera->tryGetSphericalTargetState(orbitState); + if (hasOrbitTarget) + { + auto target = hlsl::CCameraMathUtilities::castVector(orbitState.target); + if (ImGui::InputFloat3("Target", &target[0])) + activeCamera->trySetSphericalTarget(hlsl::CCameraMathUtilities::castVector(target)); + + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Target model", "Set orbit target to the model position")) + { + const auto targetPos = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(m_sceneInteraction.model))[3]; + activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); + } + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Target origin", "Set orbit target to world origin")) + activeCamera->trySetSphericalTarget(float64_t3(0.0)); + } + else + { + ImGui::TextDisabled("Active camera is not orbit."); + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("FollowHeader", "Follow Target", panelStyle.AccentColor, panelStyle); + if (auto* activeFollowConfig = getActiveFollowConfig()) + { + auto& followConfig = *activeFollowConfig; + const bool prevFollowEnabled = followConfig.enabled; + const auto prevFollowMode = followConfig.mode; + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Enable follow", .value = &followConfig.enabled, .hint = "Apply tracked-target follow to the active planar camera" }); + + const char* followModeLabels[] = { + CCameraTextUtilities::getCameraFollowModeLabel(ECameraFollowMode::Disabled), + CCameraTextUtilities::getCameraFollowModeLabel(ECameraFollowMode::OrbitTarget), + CCameraTextUtilities::getCameraFollowModeLabel(ECameraFollowMode::LookAtTarget), + CCameraTextUtilities::getCameraFollowModeLabel(ECameraFollowMode::KeepWorldOffset), + CCameraTextUtilities::getCameraFollowModeLabel(ECameraFollowMode::KeepLocalOffset) + }; + int followModeIx = static_cast(followConfig.mode); + if (ImGui::Combo("Mode", &followModeIx, followModeLabels, IM_ARRAYSIZE(followModeLabels))) + followConfig.mode = static_cast(followModeIx); + + const bool followStateChanged = followConfig.enabled != prevFollowEnabled || followConfig.mode != prevFollowMode; + if (followStateChanged && followConfig.enabled && nbl::core::CCameraFollowUtilities::cameraFollowModeUsesCapturedOffset(followConfig.mode)) + captureFollowOffsetsForPlanar(getActivePlanarIx()); + if (followStateChanged && followConfig.enabled) + applyFollowToConfiguredCameras(); + + auto trackedTarget = hlsl::CCameraMathUtilities::castVector(m_sceneInteraction.followTarget.getGimbal().getPosition()); + if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) + m_sceneInteraction.followTarget.setPosition(hlsl::CCameraMathUtilities::castVector(trackedTarget)); + + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Show target marker", .value = &m_sceneInteraction.followTargetVisible, .hint = "Render the tracked target marker in the scene" }); + + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Reset target", "Reset tracked target gimbal to the default world-space follow pose")) + resetFollowTargetToDefault(); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Snap to model", "Optionally snap tracked target gimbal to the model transform")) + snapFollowTargetToModel(); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Target origin", "Reset tracked target to identity at world origin")) + m_sceneInteraction.followTarget.setPose(float64_t3(0.0), CCameraMathUtilities::makeIdentityQuaternion()); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Capture current offset", "Store current camera-to-target relation into the active follow config")) + captureFollowOffsetsForPlanar(getActivePlanarIx()); + + if (CCameraFollowUtilities::cameraFollowModeUsesWorldOffset(followConfig.mode)) + { + auto worldOffset = hlsl::CCameraMathUtilities::castVector(followConfig.worldOffset); + if (ImGui::InputFloat3("World offset", &worldOffset[0])) + followConfig.worldOffset = hlsl::CCameraMathUtilities::castVector(worldOffset); + } + if (CCameraFollowUtilities::cameraFollowModeUsesLocalOffset(followConfig.mode)) + { + auto localOffset = hlsl::CCameraMathUtilities::castVector(followConfig.localOffset); + if (ImGui::InputFloat3("Local offset", &localOffset[0])) + followConfig.localOffset = hlsl::CCameraMathUtilities::castVector(localOffset); + } + } + else + { + ImGui::TextDisabled("No active follow config."); + } + + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelPlaybackTab.cpp b/61_UI/AppControlPanelPlaybackTab.cpp new file mode 100644 index 000000000..d1ef7c4cb --- /dev/null +++ b/61_UI/AppControlPanelPlaybackTab.cpp @@ -0,0 +1,188 @@ +#include "app/App.hpp" + +#include + +#include "app/AppControlPanelAuthoringUtilities.hpp" + +void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + using checkbox_spec_t = nbl::ui::SCameraControlPanelCheckboxSpec; + using slider_spec_t = nbl::ui::SCameraControlPanelSliderSpec; + + auto& playbackAuthoring = m_playbackAuthoring; + + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("PlaybackPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + auto* activeCamera = getActiveCamera(); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PlaybackHeader", "Playback", panelStyle.AccentColor, panelStyle); + for (const auto& spec : { + checkbox_spec_t{ .label = "Loop", .value = &playbackAuthoring.playback.loop, .hint = "Loop playback when it reaches the end" }, + checkbox_spec_t{ .label = "Override input", .value = &playbackAuthoring.playback.overrideInput, .hint = "Ignore manual input during playback" }, + checkbox_spec_t{ .label = "Affect all cameras", .value = &playbackAuthoring.affectsAll, .hint = "Apply playback to all cameras" } + }) + { + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint(spec); + } + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint({ + .label = "Speed", + .value = &playbackAuthoring.playback.speed, + .minValue = SCameraAppAuthoringDefaults::PlaybackSpeedMin, + .maxValue = SCameraAppAuthoringDefaults::PlaybackSpeedMax, + .format = "%.2f", + .hint = "Playback speed multiplier" + }); + + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint(playbackAuthoring.playback.playing ? "Pause" : "Play", "Start or pause playback")) + playbackAuthoring.playback.playing = !playbackAuthoring.playback.playing; + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Stop", "Stop playback and reset time")) + { + nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(playbackAuthoring.playback); + applyPlaybackAtTime(playbackAuthoring.playback.time); + } + + if (!playbackAuthoring.keyframeTrack.keyframes.empty()) + { + const float duration = nbl::core::CCameraPlaybackTimelineUtilities::getPlaybackTrackDuration(playbackAuthoring.keyframeTrack); + if (ImGui::SliderFloat("Time", &playbackAuthoring.playback.time, 0.f, duration, "%.3f")) + applyPlaybackAtTime(playbackAuthoring.playback.time); + } + nbl::ui::drawApplyStatusBanner( + playbackAuthoring.applyBanner.summary, + playbackAuthoring.applyBanner.succeeded, + playbackAuthoring.applyBanner.approximate, + panelStyle); + if (!playbackAuthoring.keyframeTrack.keyframes.empty()) + { + CameraPreset playbackPreviewPreset; + if (tryBuildPlaybackPresetAtTime(playbackAuthoring.playback.time, playbackPreviewPreset)) + { + const auto playbackPreviewUi = analyzePresetForUi(activeCamera, playbackPreviewPreset); + nbl::ui::CCameraControlPanelUiUtilities::drawPolicyStatus({ + .label = "Preview", + .value = playbackPreviewUi.policyLabel, + .active = playbackPreviewUi.canApply + }, panelStyle); + } + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("KeyframesHeader", "Keyframes", panelStyle.AccentColor, panelStyle); + ImGui::InputFloat("New keyframe time", &playbackAuthoring.newKeyframeTime, SCameraAppAuthoringDefaults::KeyframeTimeStep, SCameraAppAuthoringDefaults::KeyframeTimeFastStep, "%.3f"); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Time value for new keyframe"); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Use playback time", "Set new keyframe time from current playback position")) + playbackAuthoring.newKeyframeTime = playbackAuthoring.playback.time; + const auto keyframeCaptureUi = analyzeCameraCaptureForUi(activeCamera); + if (!keyframeCaptureUi.canCapture) + ImGui::BeginDisabled(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Add keyframe", keyframeCaptureUi.canCapture ? "Add keyframe from current camera" : "Keyframe capture is blocked because there is no active camera or the current goal state is invalid")) + { + CameraKeyframe keyframe; + const float authoredTime = std::max(0.f, playbackAuthoring.newKeyframeTime); + keyframe.time = authoredTime; + playbackAuthoring.newKeyframeTime = authoredTime; + if (nbl::core::CCameraPresetFlowUtilities::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) + { + playbackAuthoring.keyframeTrack.keyframes.emplace_back(std::move(keyframe)); + sortKeyframesByTime(); + selectKeyframeNearestTime(authoredTime); + } + } + if (!keyframeCaptureUi.canCapture) + ImGui::EndDisabled(); + nbl::ui::CCameraControlPanelUiUtilities::drawPolicyStatus({ + .label = "Capture", + .value = keyframeCaptureUi.policyLabel, + .active = keyframeCaptureUi.canCapture + }, panelStyle); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Clear keyframes", "Remove all keyframes")) + { + playbackAuthoring.keyframeTrack = {}; + nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(playbackAuthoring.playback); + clearApplyStatusBanner(playbackAuthoring.applyBanner); + } + + if (!playbackAuthoring.keyframeTrack.keyframes.empty()) + { + normalizeSelectedKeyframe(); + if (ImGui::BeginChild("KeyframeList", ImVec2(0.0f, panelStyle.KeyframeListHeight), true)) + { + for (size_t i = 0; i < playbackAuthoring.keyframeTrack.keyframes.size(); ++i) + { + const auto label = nbl::ui::buildKeyframeLabel(i, playbackAuthoring.keyframeTrack.keyframes[i]); + if (ImGui::Selectable(label.c_str(), playbackAuthoring.keyframeTrack.selectedKeyframeIx == static_cast(i))) + playbackAuthoring.keyframeTrack.selectedKeyframeIx = static_cast(i); + } + } + ImGui::EndChild(); + + if (auto* selectedKeyframe = getSelectedKeyframe()) + { + const auto keyframeUi = analyzePresetForUi(activeCamera, selectedKeyframe->preset); + float selectedTime = selectedKeyframe->time; + if (ImGui::InputFloat("Selected time", &selectedTime, SCameraAppAuthoringDefaults::KeyframeTimeStep, SCameraAppAuthoringDefaults::KeyframeTimeFastStep, "%.3f")) + { + selectedTime = std::max(0.f, selectedTime); + selectedKeyframe->time = selectedTime; + sortKeyframesByTime(); + selectKeyframeNearestTime(selectedTime); + clampPlaybackTimeToKeyframes(); + } + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Edit selected keyframe time"); + + nbl::ui::drawGoalApplyPresentationSummary(keyframeUi, panelStyle); + + if (!keyframeUi.canApply) + ImGui::BeginDisabled(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Apply selected", keyframeUi.canApply ? "Apply selected keyframe to the active camera" : "Apply is blocked because there is no active camera or the keyframe goal is invalid")) + applyPresetFromUi(activeCamera, selectedKeyframe->preset); + if (!keyframeUi.canApply) + ImGui::EndDisabled(); + ImGui::SameLine(); + if (!keyframeCaptureUi.canCapture) + ImGui::BeginDisabled(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Replace from camera", keyframeCaptureUi.canCapture ? "Overwrite selected keyframe from the current active camera" : "Replace is blocked because there is no active camera or the current goal state is invalid")) + replaceSelectedKeyframeFromCamera(activeCamera); + if (!keyframeCaptureUi.canCapture) + ImGui::EndDisabled(); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Jump to selected", "Set playback time to selected keyframe and preview it")) + { + playbackAuthoring.playback.time = selectedKeyframe->time; + applyPlaybackAtTime(playbackAuthoring.playback.time); + } + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Remove selected", "Remove selected keyframe")) + { + playbackAuthoring.keyframeTrack.keyframes.erase(playbackAuthoring.keyframeTrack.keyframes.begin() + playbackAuthoring.keyframeTrack.selectedKeyframeIx); + normalizeSelectedKeyframe(); + clampPlaybackTimeToKeyframes(); + if (playbackAuthoring.keyframeTrack.keyframes.empty()) + clearApplyStatusBanner(playbackAuthoring.applyBanner); + } + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::inputTextString("Keyframe file", playbackAuthoring.keyframePath); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Save keyframes", "Save keyframes to JSON file")) + { + if (!saveKeyframesToFile(nbl::system::path(playbackAuthoring.keyframePath))) + m_logger->log("Failed to save keyframes to \"%s\".", ILogger::ELL_ERROR, playbackAuthoring.keyframePath.c_str()); + } + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Load keyframes", "Load keyframes from JSON file")) + { + if (!loadKeyframesFromFile(nbl::system::path(playbackAuthoring.keyframePath))) + m_logger->log("Failed to load keyframes from \"%s\".", ILogger::ELL_ERROR, playbackAuthoring.keyframePath.c_str()); + } + } + + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelPresetsTab.cpp b/61_UI/AppControlPanelPresetsTab.cpp new file mode 100644 index 000000000..32a178745 --- /dev/null +++ b/61_UI/AppControlPanelPresetsTab.cpp @@ -0,0 +1,151 @@ +#include "app/App.hpp" + +#include +#include + +#include "app/AppControlPanelAuthoringUtilities.hpp" + +void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + auto& presetAuthoring = m_presetAuthoring; + + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("PresetsPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PresetsHeader", "Presets", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::inputTextString("Preset name", presetAuthoring.presetName); + auto* activeCamera = getActiveCamera(); + const auto presetCaptureUi = analyzeCameraCaptureForUi(activeCamera); + if (!presetCaptureUi.canCapture) + ImGui::BeginDisabled(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Add preset", presetCaptureUi.canCapture ? "Store current camera as a preset" : "Preset capture is blocked because there is no active camera or the current goal state is invalid")) + { + CameraPreset preset; + if (nbl::core::CCameraPresetFlowUtilities::tryCapturePreset(m_cameraGoalSolver, activeCamera, presetAuthoring.presetName, preset)) + { + presetAuthoring.presets.emplace_back(std::move(preset)); + presetAuthoring.selectedPresetIx = static_cast(presetAuthoring.presets.size()) - 1; + } + } + if (!presetCaptureUi.canCapture) + ImGui::EndDisabled(); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Clear presets", "Remove all presets")) + { + presetAuthoring.presets.clear(); + presetAuthoring.selectedPresetIx = -1; + } + nbl::ui::CCameraControlPanelUiUtilities::drawPolicyStatus({ + .label = "Capture", + .value = presetCaptureUi.policyLabel, + .active = presetCaptureUi.canCapture + }, panelStyle); + + if (!presetAuthoring.presets.empty()) + { + const char* presetFilterLabels[] = { + nbl::ui::CCameraPresentationUtilities::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), + nbl::ui::CCameraPresentationUtilities::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), + nbl::ui::CCameraPresentationUtilities::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) + }; + int presetFilterIx = static_cast(presetAuthoring.filterMode); + if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) + presetAuthoring.filterMode = static_cast(presetFilterIx); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); + + std::vector filteredPresetIndices; + filteredPresetIndices.reserve(presetAuthoring.presets.size()); + for (size_t i = 0; i < presetAuthoring.presets.size(); ++i) + { + if (presetMatchesFilter(activeCamera, presetAuthoring.presets[i])) + filteredPresetIndices.push_back(static_cast(i)); + } + + if (filteredPresetIndices.empty()) + { + ImGui::TextDisabled("No presets match the current filter."); + } + else + { + if (presetAuthoring.selectedPresetIx < 0 || std::find(filteredPresetIndices.begin(), filteredPresetIndices.end(), presetAuthoring.selectedPresetIx) == filteredPresetIndices.end()) + presetAuthoring.selectedPresetIx = filteredPresetIndices.front(); + + int selectedFilteredPresetIx = 0; + for (int i = 0; i < static_cast(filteredPresetIndices.size()); ++i) + { + if (filteredPresetIndices[i] == presetAuthoring.selectedPresetIx) + { + selectedFilteredPresetIx = i; + break; + } + } + + if (ImGui::BeginListBox("Preset list", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * SCameraAppAuthoringDefaults::PresetListVisibleEntries))) + { + for (int i = 0; i < static_cast(filteredPresetIndices.size()); ++i) + { + const int presetIx = filteredPresetIndices[static_cast(i)]; + const bool isSelected = selectedFilteredPresetIx == i; + const auto& presetName = presetAuthoring.presets[static_cast(presetIx)].name; + if (ImGui::Selectable(presetName.c_str(), isSelected)) + { + selectedFilteredPresetIx = i; + presetAuthoring.selectedPresetIx = presetIx; + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndListBox(); + } + + if (presetAuthoring.selectedPresetIx >= 0 && static_cast(presetAuthoring.selectedPresetIx) < presetAuthoring.presets.size()) + { + const auto& preset = presetAuthoring.presets[static_cast(presetAuthoring.selectedPresetIx)]; + const auto presetUi = analyzePresetForUi(activeCamera, preset); + nbl::ui::drawGoalApplyPresentationSummary(presetUi, panelStyle); + + if (!presetUi.canApply) + ImGui::BeginDisabled(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Apply preset", presetUi.canApply ? "Apply selected preset to the active camera" : "Apply is blocked because there is no active camera or the preset goal is invalid")) + applyPresetFromUi(activeCamera, preset); + if (!presetUi.canApply) + ImGui::EndDisabled(); + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Remove preset", "Remove selected preset")) + { + presetAuthoring.presets.erase(presetAuthoring.presets.begin() + presetAuthoring.selectedPresetIx); + presetAuthoring.selectedPresetIx = -1; + } + } + } + } + + nbl::ui::drawApplyStatusBanner( + presetAuthoring.applyBanner.summary, + presetAuthoring.applyBanner.succeeded, + presetAuthoring.applyBanner.approximate, + panelStyle); + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PresetsStorageHeader", "Storage", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::inputTextString("Preset file", presetAuthoring.presetPath); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Save presets", "Save presets to JSON file")) + { + if (!savePresetsToFile(nbl::system::path(presetAuthoring.presetPath))) + m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, presetAuthoring.presetPath.c_str()); + } + ImGui::SameLine(); + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Load presets", "Load presets from JSON file")) + { + if (!loadPresetsFromFile(nbl::system::path(presetAuthoring.presetPath))) + m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, presetAuthoring.presetPath.c_str()); + } + + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelProjectionTab.cpp b/61_UI/AppControlPanelProjectionTab.cpp new file mode 100644 index 000000000..0cc2a2a95 --- /dev/null +++ b/61_UI/AppControlPanelProjectionTab.cpp @@ -0,0 +1,94 @@ +#include "app/App.hpp" +#include "app/AppProjectionControlPanelUiUtilities.hpp" + +void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("ProjectionPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PlanarSelectHeader", "Planar Selection", panelStyle.AccentColor, panelStyle); + + SActiveProjectionTabContext runtime = {}; + auto refreshRuntime = [&]() -> bool + { + return tryBuildActiveProjectionTabContext(runtime); + }; + + if (!nbl::ui::drawRenderWindowSelector(m_viewports.windowBindings.size(), m_viewports.activeRenderWindowIx, refreshRuntime)) + { + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Choose which render window the panel edits"); + + if (!refreshRuntime()) + { + ImGui::TextDisabled("No active viewport."); + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + ImGui::Text("Editing: %s", runtime.activeRenderWindowIxString.c_str()); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Selected render window for planar and projection changes"); + + assert(!m_planarProjections.empty()); + auto& binding = runtime.requireBinding(); + if (!nbl::ui::drawProjectionPlanarSelector(getPlanarProjectionSpan(), runtime, refreshRuntime)) + { + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Select which camera the window renders"); + + assert(binding.boundProjectionIx.has_value()); + assert(binding.lastBoundPerspectivePresetProjectionIx.has_value()); + assert(binding.lastBoundOrthoPresetProjectionIx.has_value()); + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("ProjectionParamsHeader", "Projection Parameters", panelStyle.AccentColor, panelStyle); + if (!nbl::ui::drawProjectionTypeSelector(getPlanarProjectionSpan(), runtime, refreshRuntime)) + { + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + const auto selectedProjectionType = runtime.requirePlanar().getPlanarProjections()[binding.boundProjectionIx.value()].getParameters().m_type; + const bool updateBoundVirtualMaps = nbl::ui::drawProjectionPresetSelector(getPlanarProjectionSpan(), runtime, selectedProjectionType); + if (updateBoundVirtualMaps) + syncWindowInputBinding(binding); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Switch preset projection for this planar"); + + auto& boundProjection = runtime.requirePlanar().getPlanarProjections()[binding.boundProjectionIx.value()]; + assert(!boundProjection.isProjectionSingular()); + nbl::ui::drawProjectionParameterControls(binding, boundProjection, m_viewports.useWindow); + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("CursorHeader", "Cursor Behaviour", panelStyle.AccentColor, panelStyle); + nbl::ui::drawCursorBehaviourControls(m_viewports.captureCursorInMoveMode, m_viewports.resetCursorToCenter); + + ImGui::TextColored( + m_viewports.enableActiveCameraMovement ? panelStyle.GoodColor : panelStyle.BadColor, + "Bound Camera Movement: %s", + m_viewports.enableActiveCameraMovement ? "Enabled" : "Disabled"); + ImGui::Separator(); + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("BoundCameraHeader", "Bound Camera", panelStyle.AccentColor, panelStyle); + nbl::ui::drawBoundCameraSection( + runtime, + binding.activePlanarIx, + [this](const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator) + { + addMatrixTable(topText, tableName, rows, columns, pointer, withSeparator); + }, + [this](SWindowControlBinding& windowBinding) { syncWindowInputBinding(windowBinding); }, + [this](SWindowControlBinding& windowBinding) { syncWindowInputBindingToProjection(windowBinding); }); + + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelStatusTab.cpp b/61_UI/AppControlPanelStatusTab.cpp new file mode 100644 index 000000000..690975ae6 --- /dev/null +++ b/61_UI/AppControlPanelStatusTab.cpp @@ -0,0 +1,113 @@ +#include "app/App.hpp" + +#include + +void App::drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("StatusPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("SessionHeader", "Session", panelStyle.AccentColor, panelStyle); + if (nbl::ui::CCameraControlPanelUiUtilities::beginCard("SessionCard", nbl::ui::CCameraControlPanelUiUtilities::calcCameraControlPanelCardHeight(3, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + if (ImGui::BeginTable("SessionTable", 2, panelStyle.SummaryTableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + const auto activeWindowText = std::to_string(m_viewports.activeRenderWindowIx); + const std::array sessionRows = {{ + { .label = "Mode", .value = m_viewports.useWindow ? "Window" : "Fullscreen", .dotColor = panelStyle.AccentColor, .valueColor = panelStyle.AccentColor }, + { .label = "Active window", .value = activeWindowText, .dotColor = panelStyle.AccentColor, .valueColor = panelStyle.AccentColor }, + { .label = "Movement", .value = m_viewports.enableActiveCameraMovement ? "Enabled" : "Disabled", .dotColor = m_viewports.enableActiveCameraMovement ? panelStyle.GoodColor : panelStyle.BadColor, .valueColor = m_viewports.enableActiveCameraMovement ? panelStyle.GoodColor : panelStyle.BadColor } + }}; + for (const auto& row : sessionRows) + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine(row, panelStyle); + ImGui::EndTable(); + } + } + nbl::ui::CCameraControlPanelUiUtilities::endCard(); + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("CameraHeader", "Camera", panelStyle.AccentColor, panelStyle); + if (auto* activeCamera = getActiveCamera()) + { + const auto& gimbal = activeCamera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + + if (nbl::ui::CCameraControlPanelUiUtilities::beginCard("CameraCard", nbl::ui::CCameraControlPanelUiUtilities::calcCameraControlPanelCardHeight(5, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + if (ImGui::BeginTable("CameraTable", 2, panelStyle.SummaryTableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + const auto positionText = std::format("{:.2f} {:.2f} {:.2f}", pos.x, pos.y, pos.z); + const auto eulerText = std::format("{:.1f} {:.1f} {:.1f}", euler.x, euler.y, euler.z); + const auto moveScaleText = std::format("{:.4f}", activeCamera->getMoveSpeedScale()); + const auto rotateScaleText = std::format("{:.4f}", activeCamera->getRotationSpeedScale()); + const std::array cameraRows = {{ + { .label = "Name", .value = activeCamera->getIdentifier(), .dotColor = panelStyle.AccentColor, .valueColor = panelStyle.MutedColor }, + { .label = "Position", .value = positionText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, + { .label = "Euler", .value = eulerText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, + { .label = "Move scale", .value = moveScaleText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, + { .label = "Rotate scale", .value = rotateScaleText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor } + }}; + for (const auto& row : cameraRows) + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine(row, panelStyle); + ImGui::EndTable(); + } + } + nbl::ui::CCameraControlPanelUiUtilities::endCard(); + } + else if (nbl::ui::CCameraControlPanelUiUtilities::beginCard("CameraCard", nbl::ui::CCameraControlPanelUiUtilities::calcCameraControlPanelCardHeight(2, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + ImGui::TextDisabled("No active camera"); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("ProjectionHeader", "Projection", panelStyle.AccentColor, panelStyle); + auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (planar && binding.boundProjectionIx.has_value()) + { + auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; + const auto& params = projection.getParameters(); + if (nbl::ui::CCameraControlPanelUiUtilities::beginCard("ProjectionCard", nbl::ui::CCameraControlPanelUiUtilities::calcCameraControlPanelCardHeight(4, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + if (ImGui::BeginTable("ProjectionTable", 2, panelStyle.SummaryTableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + const auto zNearText = std::format("{:.2f}", params.m_zNear); + const auto zFarText = std::format("{:.2f}", params.m_zFar); + const auto typeText = params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"; + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine({ .label = "Type", .value = typeText, .dotColor = panelStyle.AccentColor, .valueColor = panelStyle.MutedColor }, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine({ .label = "zNear", .value = zNearText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine({ .label = "zFar", .value = zFarText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + if (params.m_type == IPlanarProjection::CProjection::Perspective) + { + const auto fovText = std::format("{:.1f}", params.m_planar.perspective.fov); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine({ .label = "Fov", .value = fovText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + } + else + { + const auto orthoWidthText = std::format("{:.1f}", params.m_planar.orthographic.orthoWidth); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine({ .label = "Ortho width", .value = orthoWidthText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + } + ImGui::EndTable(); + } + } + nbl::ui::CCameraControlPanelUiUtilities::endCard(); + } + else if (nbl::ui::CCameraControlPanelUiUtilities::beginCard("ProjectionCard", nbl::ui::CCameraControlPanelUiUtilities::calcCameraControlPanelCardHeight(2, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + ImGui::TextDisabled("No projection bound"); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); + } + + ImGui::PopItemWidth(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelTabs.cpp b/61_UI/AppControlPanelTabs.cpp new file mode 100644 index 000000000..322d6a775 --- /dev/null +++ b/61_UI/AppControlPanelTabs.cpp @@ -0,0 +1,198 @@ +#include "app/App.hpp" + +#include +#include +#include + +using control_panel_style_t = nbl::ui::SCameraControlPanelStyle; + +enum class EControlPanelToggleBinding : uint8_t +{ + UseWindow, + ShowHud, + ShowEventLog +}; + +enum class EControlPanelTabGate : uint8_t +{ + Always, + ShowHud, + ShowEventLog +}; + +struct SControlPanelToggleDescriptor final +{ + const char* label = ""; + const char* hint = ""; + EControlPanelToggleBinding binding = EControlPanelToggleBinding::UseWindow; +}; + +struct SControlPanelTabEntry final +{ + const char* label = ""; + EControlPanelTabGate gate = EControlPanelTabGate::Always; + void (App::*draw)(const nbl::ui::SCameraControlPanelStyle&) = nullptr; +}; + +inline constexpr std::array ControlPanelToggles = {{ + { "WINDOW", "Toggle split render windows", EControlPanelToggleBinding::UseWindow }, + { "STATUS", "Show system and camera status panel", EControlPanelToggleBinding::ShowHud }, + { "EVENT LOG", "Show virtual event log", EControlPanelToggleBinding::ShowEventLog } +}}; + +inline float calcControlPanelToggleRowWidth( + std::span toggles, + const control_panel_style_t& panelStyle, + const float gap) +{ + float rowWidth = 0.0f; + for (size_t toggleIx = 0u; toggleIx < toggles.size(); ++toggleIx) + { + if (toggleIx > 0u) + rowWidth += gap; + rowWidth += nbl::ui::CCameraControlPanelUiUtilities::calcPillWidth(toggles[toggleIx].label, panelStyle.TogglePadding); + } + return rowWidth; +} + +void App::drawControlPanelHeader(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, panelStyle.CardChildRounding); + if (ImGui::BeginChild("PanelHeader", ImVec2(0.0f, panelStyle.HeaderWindowHeight), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderDummyY)); + ImGui::SetWindowFontScale(panelStyle.HeaderTitleFontScale); + ImGui::TextColored(panelStyle.AccentColor, "Control Panel"); + ImGui::SetWindowFontScale(1.0f); + + const float gap = ImGui::GetStyle().ItemSpacing.x; + std::array headerBadges = {{ + { m_viewports.useWindow ? "WINDOW" : "FULL", panelStyle.AccentColor }, + { m_viewports.enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF", m_viewports.enableActiveCameraMovement ? panelStyle.GoodColor : panelStyle.BadColor }, + { m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF", m_scriptedInput.enabled ? panelStyle.AccentColor : panelStyle.InactiveBadgeColor }, + { "CI", panelStyle.WarnColor } + }}; + const size_t headerBadgeCount = m_cliRuntime.ciMode ? headerBadges.size() : headerBadges.size() - 1u; + nbl::ui::CCameraControlPanelUiUtilities::drawBadgeRow(std::span(headerBadges.data(), headerBadgeCount), panelStyle.BadgeTextColor, gap, panelStyle); + + ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); + const std::array keyHintGroups = {{ + { "Move", nbl::ui::SCameraControlPanelHeaderHints::MoveKeys }, + { "Look", nbl::ui::SCameraControlPanelHeaderHints::LookKeys }, + { "Zoom", nbl::ui::SCameraControlPanelHeaderHints::ZoomKeys } + }}; + nbl::ui::CCameraControlPanelUiUtilities::drawKeyHintGroupRow(keyHintGroups, gap, gap * 2.0f, panelStyle.KeyBackgroundColor, panelStyle.KeyTextColor, panelStyle); + + ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); + if (ImGui::BeginTable("HeaderMetrics", 3, ImGuiTableFlags_SizingStretchProp)) + { + const float frameMs = std::max(0.0f, m_uiMetrics.lastFrameMs); + const float fps = nbl::ui::CCameraControlPanelUiUtilities::calcFramesPerSecond(frameMs, panelStyle); + const std::array miniStats = {{ + { "FrameStat", "Frame", panelStyle.AccentColor, panelStyle.DefaultFrameMetricMin }, + { "InputStat", "Input", panelStyle.AccentColor, panelStyle.DefaultEventMetricMin }, + { "VirtualStat", "Virtual", panelStyle.AccentColor, panelStyle.DefaultEventMetricMin } + }}; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + nbl::ui::CCameraControlPanelUiUtilities::drawMiniStat(miniStats[0], m_uiMetrics.frameMs, m_uiMetrics.sampleIndex, [&] + { + ImGui::TextColored(panelStyle.AccentColor, "%.1f ms %.0f fps", frameMs, fps); + }, panelStyle); + + ImGui::TableSetColumnIndex(1); + nbl::ui::CCameraControlPanelUiUtilities::drawMiniStat(miniStats[1], m_uiMetrics.inputCounts, m_uiMetrics.sampleIndex, [&] + { + ImGui::TextColored(panelStyle.AccentColor, "%u ev", m_uiMetrics.lastInputEvents); + }, panelStyle); + + ImGui::TableSetColumnIndex(2); + nbl::ui::CCameraControlPanelUiUtilities::drawMiniStat(miniStats[2], m_uiMetrics.virtualCounts, m_uiMetrics.sampleIndex, [&] + { + ImGui::TextColored(panelStyle.AccentColor, "%u ev", m_uiMetrics.lastVirtualEvents); + }, panelStyle); + ImGui::EndTable(); + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(); +} + +void App::drawControlPanelToggles(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + const float gap = ImGui::GetStyle().ItemSpacing.x; + const auto getToggleValue = [&](const EControlPanelToggleBinding binding) -> bool& + { + switch (binding) + { + case EControlPanelToggleBinding::UseWindow: + return m_viewports.useWindow; + case EControlPanelToggleBinding::ShowHud: + return m_eventLog.showHud; + case EControlPanelToggleBinding::ShowEventLog: + default: + return m_eventLog.showEventLog; + } + }; + + const float rowWidth = calcControlPanelToggleRowWidth(ControlPanelToggles, panelStyle, gap); + nbl::ui::CCameraControlPanelUiUtilities::centerControlPanelRow(rowWidth); + for (size_t toggleIx = 0u; toggleIx < ControlPanelToggles.size(); ++toggleIx) + { + if (toggleIx > 0u) + ImGui::SameLine(0.0f, gap); + + const auto& toggle = ControlPanelToggles[toggleIx]; + nbl::ui::CCameraControlPanelUiUtilities::drawTogglePill( + toggle.label, + getToggleValue(toggle.binding), + panelStyle.AccentColor, + panelStyle.InactiveBadgeColor, + panelStyle.BadgeTextColor, + panelStyle.TogglePadding); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint(toggle.hint); + } +} + +void App::drawControlPanelTabs(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + static constexpr std::array ControlPanelTabs = {{ + { "Status", EControlPanelTabGate::ShowHud, &App::drawControlPanelStatusTab }, + { "Projection", EControlPanelTabGate::Always, &App::drawControlPanelProjectionTab }, + { "Camera", EControlPanelTabGate::Always, &App::drawControlPanelCameraTab }, + { "Presets", EControlPanelTabGate::Always, &App::drawControlPanelPresetsTab }, + { "Playback", EControlPanelTabGate::Always, &App::drawControlPanelPlaybackTab }, + { "Gizmo", EControlPanelTabGate::Always, &App::drawControlPanelGizmoTab }, + { "Log", EControlPanelTabGate::ShowEventLog, &App::drawControlPanelLogTab } + }}; + + if (!ImGui::BeginTabBar("ControlTabs")) + return; + + const auto isTabEnabled = [&](const EControlPanelTabGate gate) -> bool + { + switch (gate) + { + case EControlPanelTabGate::ShowHud: + return m_eventLog.showHud; + case EControlPanelTabGate::ShowEventLog: + return m_eventLog.showEventLog; + case EControlPanelTabGate::Always: + default: + return true; + } + }; + + for (const auto& tab : ControlPanelTabs) + { + if (!isTabEnabled(tab.gate) || !ImGui::BeginTabItem(tab.label)) + continue; + + (this->*tab.draw)(panelStyle); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); +} + diff --git a/61_UI/AppControlPanelUtilityTabs.cpp b/61_UI/AppControlPanelUtilityTabs.cpp new file mode 100644 index 000000000..f19d7ae91 --- /dev/null +++ b/61_UI/AppControlPanelUtilityTabs.cpp @@ -0,0 +1,52 @@ +#include "app/App.hpp" + +void App::drawControlPanelGizmoTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("GizmoPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("GizmoHeader", "Gizmo", panelStyle.AccentColor, panelStyle); + TransformEditorContents(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} + +void App::drawControlPanelLogTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + auto& eventLog = m_eventLog; + + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("LogPanel", panelStyle)) + { + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); + return; + } + + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("LogHeader", "Virtual Events", panelStyle.AccentColor, panelStyle); + ImGui::Checkbox("Auto-scroll", &eventLog.autoScroll); + ImGui::SameLine(); + ImGui::Checkbox("Wrap", &eventLog.wrap); + ImGui::Separator(); + + const ImGuiWindowFlags logFlags = eventLog.wrap ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar; + if (ImGui::BeginChild("LogList", ImVec2(0.0f, 0.0f), false, logFlags)) + { + const float scrollY = ImGui::GetScrollY(); + const float scrollMax = ImGui::GetScrollMaxY(); + const bool wasAtBottom = scrollY >= scrollMax - panelStyle.EventLogBottomThreshold; + const size_t start = eventLog.entries.size() > SCameraAppAuthoringDefaults::EventLogVisibleEntries ? + eventLog.entries.size() - SCameraAppAuthoringDefaults::EventLogVisibleEntries : + 0u; + if (eventLog.wrap) + ImGui::PushTextWrapPos(0.0f); + for (size_t i = start; i < eventLog.entries.size(); ++i) + ImGui::TextUnformatted(eventLog.entries[i].line.c_str()); + if (eventLog.wrap) + ImGui::PopTextWrapPos(); + if (eventLog.autoScroll && wasAtBottom && !eventLog.entries.empty()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); +} diff --git a/61_UI/AppDebugSceneRendererResources.cpp b/61_UI/AppDebugSceneRendererResources.cpp new file mode 100644 index 000000000..0b9fd5b56 --- /dev/null +++ b/61_UI/AppDebugSceneRendererResources.cpp @@ -0,0 +1,52 @@ +#include "app/App.hpp" + +bool App::initializeGeometrySceneResources() +{ + const uint32_t additionalBufferOwnershipFamilies[] = { getGraphicsQueue()->getFamilyIndex() }; + m_debugScene.scene = CGeometryCreatorScene::create( + { + .transferQueue = getTransferUpQueue(), + .utilities = m_utils.get(), + .logger = m_logger.get(), + .addtionalBufferOwnershipFamilies = additionalBufferOwnershipFamilies + }, + CSimpleDebugRenderer::DefaultPolygonGeometryPatch); + + return m_debugScene.scene || logFail("Could not create geometry creator scene!"); +} + +bool App::initializeDebugSceneRendererResources() +{ + const auto& geometries = m_debugScene.scene->getInitParams().geometries; + if (geometries.empty()) + return logFail("No geometries found for scene!"); + + m_debugScene.renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), m_debugScene.renderpass.get(), 0, { &geometries.front().get(), geometries.size() }); + if (!m_debugScene.renderer) + return logFail("Failed to create debug renderer!"); + + const auto& pipelines = m_debugScene.renderer->getInitParams().pipelines; + m_debugScene.gridGeometryIx = std::nullopt; + m_debugScene.followTargetGeometryIx = std::nullopt; + auto ix = 0u; + for (const auto& name : m_debugScene.scene->getInitParams().geometryNames) + { + if (name == "Cube") + { + if (!m_debugScene.followTargetGeometryIx.has_value()) + m_debugScene.followTargetGeometryIx = ix; + } + else if (name == "Cone") + { + m_debugScene.renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; + } + else if (name == "Grid") + { + m_debugScene.gridGeometryIx = ix; + } + ++ix; + } + + m_debugScene.renderer->m_instances.resize(1u + (m_debugScene.gridGeometryIx.has_value() ? 1u : 0u) + (m_debugScene.followTargetGeometryIx.has_value() ? 1u : 0u)); + return true; +} diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp new file mode 100644 index 000000000..984f8ce43 --- /dev/null +++ b/61_UI/AppFollowRuntime.cpp @@ -0,0 +1,260 @@ +#include "app/App.hpp" + +inline float getScriptVisualDebugFps(const SScriptedInputRuntimeState& scriptedInput) +{ + return std::max(1.f, scriptedInput.visualTargetFps); +} + +inline uint64_t computeScriptVisualDebugHoldFrames(const SScriptedInputRuntimeState& scriptedInput, const float fps) +{ + return static_cast(std::round(std::max(0.f, scriptedInput.visualCameraHoldSeconds) * fps)); +} + +inline uint64_t computeElapsedFrames(const uint64_t currentFrame, const SScriptedVisualPlanarState& visualPlanar) +{ + return (currentFrame >= visualPlanar.startFrame) ? (currentFrame - visualPlanar.startFrame) : 0ull; +} + +inline uint64_t computeProgressFrames(const uint64_t elapsedFrames, const uint64_t holdFrames) +{ + return holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; +} + +inline nbl::ui::SCameraScriptVisualDebugStatus buildScriptVisualDebugStatus( + const ICamera& camera, + const uint32_t planarIx, + const size_t planarCount, + const uint64_t absoluteFrame, + const SScriptedInputRuntimeState& scriptedInput) +{ + const auto fps = getScriptVisualDebugFps(scriptedInput); + const auto holdFrames = computeScriptVisualDebugHoldFrames(scriptedInput, fps); + const auto elapsedFrames = computeElapsedFrames(absoluteFrame, scriptedInput.visualPlanar); + + nbl::ui::SCameraScriptVisualDebugStatus status = {}; + status.cameraLabel = CCameraTextUtilities::getCameraTypeLabel(&camera); + status.cameraHint = CCameraTextUtilities::getCameraTypeDescription(&camera); + status.cameraIndex = planarIx; + status.cameraCount = static_cast(planarCount); + status.planarIndex = planarIx; + status.hasHoldFrames = holdFrames > 0u; + status.progressFrames = computeProgressFrames(elapsedFrames, holdFrames); + status.holdFrames = holdFrames; + status.targetFps = fps; + status.absoluteFrame = absoluteFrame; + status.segmentLabel = scriptedInput.visualPlanar.segmentLabel; + status.followActive = scriptedInput.visualFollow.active; + status.followModeDescription = nbl::ui::CCameraTextUtilities::getCameraFollowModeDescription(scriptedInput.visualFollow.mode); + status.followLockValid = scriptedInput.visualFollow.lockValid; + status.followLockAngleDeg = scriptedInput.visualFollow.lockAngleDeg; + status.followTargetDistance = scriptedInput.visualFollow.targetDistance; + status.followTargetCenterNdcRadius = scriptedInput.visualFollow.projectedTarget.radius; + + float dynamicFov = 0.0f; + if (camera.tryGetDynamicPerspectiveFov(dynamicFov)) + { + status.hasDynamicFov = true; + status.dynamicFovDeg = dynamicFov; + } + + return status; +} + +inline float getFollowTargetMarkerScale(const SScriptedInputRuntimeState& scriptedInput) +{ + return (scriptedInput.enabled && scriptedInput.visualDebug) ? + SCameraAppSceneDefaults::FollowTargetMarkerScaleVisualDebug : + SCameraAppSceneDefaults::FollowTargetMarkerScale; +} + +void App::setFollowTargetTransform(const float64_t4x4& transform) +{ + m_sceneInteraction.followTarget.trySetFromTransform(transform); +} + +float32_t3x4 App::computeFollowTargetMarkerWorld() const +{ + return buildFollowTargetMarkerWorldTransform( + m_sceneInteraction.followTarget, + getFollowTargetMarkerScale(m_scriptedInput)); +} + +bool App::captureFollowOffsetsForPlanar(const uint32_t planarIx) +{ + if (planarIx >= m_planarProjections.size() || planarIx >= m_sceneInteraction.planarFollowConfigs.size()) + return false; + + auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + return nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera( + m_cameraGoalSolver, + camera, + m_sceneInteraction.followTarget, + m_sceneInteraction.planarFollowConfigs[planarIx]); +} + +bool App::followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const +{ + return config.enabled && nbl::core::CCameraFollowUtilities::cameraFollowModeUsesCapturedOffset(config.mode); +} + +void App::refreshFollowOffsetConfigForPlanar(const uint32_t planarIx) +{ + if (planarIx >= m_planarProjections.size() || planarIx >= m_sceneInteraction.planarFollowConfigs.size()) + return; + + auto& config = m_sceneInteraction.planarFollowConfigs[planarIx]; + if (!followConfigUsesCapturedOffset(config)) + return; + + auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + if (!camera) + return; + + nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); +} + +void App::refreshFollowOffsetConfigsForCamera(ICamera* camera) +{ + if (!camera) + return; + + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size() && planarIx < m_sceneInteraction.planarFollowConfigs.size(); ++planarIx) + { + auto* planarCamera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + if (planarCamera != camera) + continue; + refreshFollowOffsetConfigForPlanar(planarIx); + } +} + +void App::refreshAllFollowOffsetConfigs() +{ + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size() && planarIx < m_sceneInteraction.planarFollowConfigs.size(); ++planarIx) + refreshFollowOffsetConfigForPlanar(planarIx); +} + +float64_t3 App::getDefaultFollowTargetPosition() const +{ + return SCameraAppSceneDefaults::DefaultFollowTargetPosition; +} + +camera_quaternion_t App::getDefaultFollowTargetOrientation() const +{ + return SCameraAppSceneDefaults::DefaultFollowTargetOrientation; +} + +void App::resetFollowTargetToDefault() +{ + m_sceneInteraction.followTarget.setPose(getDefaultFollowTargetPosition(), getDefaultFollowTargetOrientation()); +} + +void App::snapFollowTargetToModel() +{ + const auto modelTransform = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(m_sceneInteraction.model)); + setFollowTargetTransform(getCastedMatrix(modelTransform)); +} + +void App::applyFollowToConfiguredCameras(const bool allowDuringScriptedInput) +{ + if (m_scriptedInput.enabled && !allowDuringScriptedInput) + return; + if (m_sceneInteraction.planarFollowConfigs.size() != m_planarProjections.size()) + return; + + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) + { + auto& planar = m_planarProjections[planarIx]; + auto* camera = planar ? planar->getCamera() : nullptr; + if (!camera) + continue; + + const auto& config = m_sceneInteraction.planarFollowConfigs[planarIx]; + if (!config.enabled || config.mode == ECameraFollowMode::Disabled) + continue; + + const auto result = nbl::core::CCameraFollowUtilities::applyFollowToCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); + if (!result.succeeded()) + continue; + + for (auto& projection : planar->getPlanarProjections()) + nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(camera, projection); + } +} + +bool App::isOrbitLikeCamera(ICamera* camera) +{ + return camera && camera->hasCapability(ICamera::SphericalTarget); +} + +void App::syncVisualDebugWindowBindings() +{ + if (!m_scriptedInput.enabled) + return; + if (m_viewports.windowBindings.size() < 2u || m_planarProjections.empty()) + return; + + auto& perspectiveBinding = m_viewports.windowBindings[0u]; + if (perspectiveBinding.activePlanarIx >= m_planarProjections.size()) + return; + + auto& perspectivePlanar = m_planarProjections[perspectiveBinding.activePlanarIx]; + if (!perspectivePlanar) + return; + if (!nbl::ui::trySelectBindingProjectionType( + getPlanarProjectionSpan(), + perspectiveBinding, + IPlanarProjection::CProjection::Perspective)) + { + return; + } + + auto& orthoBinding = m_viewports.windowBindings[1u]; + if (orthoBinding.activePlanarIx != perspectiveBinding.activePlanarIx) + { + if (!nbl::ui::trySelectBindingPlanar( + getPlanarProjectionSpan(), + orthoBinding, + perspectiveBinding.activePlanarIx)) + { + return; + } + } + + if (orthoBinding.activePlanarIx >= m_planarProjections.size()) + return; + + auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; + if (!orthoPlanar) + return; + + nbl::ui::trySelectBindingProjectionType( + getPlanarProjectionSpan(), + orthoBinding, + IPlanarProjection::CProjection::Orthographic); +} + +void App::drawScriptVisualDebugOverlay(const ImVec2& displaySize) +{ + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + + const auto viewportState = tryGetActiveViewportRuntimeState(); + if (!viewportState.valid()) + return; + + if (!m_scriptedInput.visualPlanar.valid) + { + m_scriptedInput.visualPlanar.valid = true; + m_scriptedInput.visualPlanar.planarIx = viewportState.requireBinding().activePlanarIx; + m_scriptedInput.visualPlanar.startFrame = m_realFrameIx; + } + + const auto debugStatus = buildScriptVisualDebugStatus( + viewportState.requireCamera(), + viewportState.requireBinding().activePlanarIx, + m_planarProjections.size(), + m_realFrameIx, + m_scriptedInput); + + nbl::ui::CCameraScriptVisualDebugOverlayUtilities::drawScriptVisualDebugOverlay(displaySize, nbl::ui::CCameraScriptVisualDebugOverlayUtilities::buildScriptVisualDebugOverlayData(debugStatus)); +} diff --git a/61_UI/AppFrameCapture.cpp b/61_UI/AppFrameCapture.cpp new file mode 100644 index 000000000..c6699e04c --- /dev/null +++ b/61_UI/AppFrameCapture.cpp @@ -0,0 +1,75 @@ +#include "app/App.hpp" + +void App::captureRenderedFrame(IGPUImage* frame, const uint64_t renderedFrameIx, const nbl::system::path& outPath, const char* tag) +{ + if (!m_device || !m_assetMgr || !m_surface || !frame) + return; + + m_logger->log("%s screenshot capture start (frame %llu).", ILogger::ELL_INFO, tag, static_cast(renderedFrameIx)); + const ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx }; + if (m_device->blockForSemaphores({ &waitInfo, &waitInfo + 1 }) != ISemaphore::WAIT_RESULT::SUCCESS) + { + m_logger->log("%s screenshot failed: wait for render finished.", ILogger::ELL_ERROR, tag); + return; + } + + auto viewParams = IGPUImageView::SCreationParams{ + .subUsages = IGPUImage::EUF_TRANSFER_SRC_BIT, + .image = core::smart_refctd_ptr(frame), + .viewType = IGPUImageView::ET_2D, + .format = frame->getCreationParameters().format + }; + viewParams.subresourceRange.aspectMask = IGPUImage::EAF_COLOR_BIT; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = 1u; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = 1u; + auto frameView = m_device->createImageView(std::move(viewParams)); + if (!frameView) + { + m_logger->log("%s screenshot failed: could not create frame view.", ILogger::ELL_ERROR, tag); + return; + } + + m_logger->log("%s screenshot capture: calling createScreenShot.", ILogger::ELL_INFO, tag); + const bool ok = ext::ScreenShot::createScreenShot( + m_device.get(), + getGraphicsQueue(), + nullptr, + frameView.get(), + m_assetMgr.get(), + outPath, + asset::IImage::LAYOUT::TRANSFER_SRC_OPTIMAL, + asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT); + + if (ok) + m_logger->log("%s screenshot saved to \"%s\".", ILogger::ELL_INFO, tag, outPath.string().c_str()); + else + m_logger->log("%s screenshot failed to save.", ILogger::ELL_ERROR, tag); +} + +void App::handleFrameCaptureRequests(IGPUImage* frame, const uint64_t renderedFrameIx) +{ + if (m_cliRuntime.ciMode && !m_cliRuntime.ciScreenshotDone) + { + ++m_cliRuntime.ciFrameCounter; + if (m_cliRuntime.ciFrameCounter >= SCameraAppRuntimeDefaults::CiFramesBeforeCapture) + { + m_cliRuntime.ciScreenshotDone = true; + if (!m_cliRuntime.disableScreenshotsCli) + captureRenderedFrame(frame, renderedFrameIx, m_cliRuntime.ciScreenshotPath, "CI"); + } + } + + if (m_cliRuntime.disableScreenshotsCli || !m_scriptedInput.enabled) + return; + + while (m_scriptedInput.nextCaptureIndex < m_scriptedInput.timeline.captureFrames.size() && + m_scriptedInput.timeline.captureFrames[m_scriptedInput.nextCaptureIndex] == renderedFrameIx) + { + const auto outPath = m_scriptedInput.captureOutputDir / + (m_scriptedInput.capturePrefix + "_" + std::to_string(renderedFrameIx) + ".png"); + captureRenderedFrame(frame, renderedFrameIx, outPath, "Script"); + ++m_scriptedInput.nextCaptureIndex; + } +} diff --git a/61_UI/AppFrameRuntime.cpp b/61_UI/AppFrameRuntime.cpp new file mode 100644 index 000000000..56eccd331 --- /dev/null +++ b/61_UI/AppFrameRuntime.cpp @@ -0,0 +1,126 @@ +#include "app/App.hpp" +#include "app/AppRenderPassUtilities.hpp" + +bool App::waitForInflightFrameSlot() +{ + const uint32_t framesInFlight = getFramesInFlight(); + if (m_realFrameIx < framesInFlight) + return true; + + const ISemaphore::SWaitInfo cmdbufDonePending[] = { + { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1 - framesInFlight + } + }; + return m_device->blockForSemaphores(cmdbufDonePending) == ISemaphore::WAIT_RESULT::SUCCESS; +} + +uint32_t App::getFramesInFlight() const +{ + return core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); +} + +std::optional App::tryBuildFrameSubmissionContext() +{ + const auto currentSwapchainExtent = m_surface->getCurrentExtent(); + if (currentSwapchainExtent.width * currentSwapchainExtent.height <= 0) + return std::nullopt; + + SFrameSubmissionContext frameContext = {}; + frameContext.resourceIx = m_realFrameIx % MaxFramesInFlight; + frameContext.renderArea = makeRenderArea(currentSwapchainExtent.width, currentSwapchainExtent.height); + frameContext.frame = m_tripleBuffers[frameContext.resourceIx].get(); + frameContext.cmdbuf = m_cmdBufs[frameContext.resourceIx].get(); + frameContext.blitWaitValue = m_blitWaitValues.data() + frameContext.resourceIx; + return frameContext; +} + +bool App::recordFramePasses(const SFrameSubmissionContext& frameContext) +{ + auto* cmdbuf = frameContext.cmdbuf; + bool success = cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + success = success && cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + success = success && cmdbuf->beginDebugMarker("UIApp Frame"); + + updateSceneDebugInstances(); + if (m_viewports.useWindow) + { + for (uint32_t bindingIx = 0u; bindingIx < m_viewports.windowBindings.size(); ++bindingIx) + success = success && recordSceneFramebufferPass(cmdbuf, m_viewports.windowBindings[bindingIx], bindingIx); + } + else + { + success = success && recordSceneFramebufferPass(cmdbuf, m_viewports.windowBindings[m_viewports.activeRenderWindowIx], m_viewports.activeRenderWindowIx); + } + + success = success && recordUiRenderPass(cmdbuf, frameContext.resourceIx); + + const auto blitQueueFamily = m_surface->getAssignedQueue()->getFamilyIndex(); + const bool needOwnershipRelease = cmdbuf->getQueueFamilyIndex() != blitQueueFamily && + !frameContext.frame->getCachedCreationParams().isConcurrentSharing(); + if (needOwnershipRelease) + { + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t barrier[] = { { + .barrier = { + .dep = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, + .srcAccessMask = asset::ACCESS_FLAGS::MEMORY_READ_BITS | asset::ACCESS_FLAGS::MEMORY_WRITE_BITS, + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .dstAccessMask = asset::ACCESS_FLAGS::NONE + }, + .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::RELEASE, + .otherQueueFamilyIndex = blitQueueFamily + }, + .image = frameContext.frame, + .subresourceRange = TripleBufferUsedSubresourceRange + } }; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = { .imgBarriers = barrier }; + success = success && cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); + } + + return success && cmdbuf->end(); +} + +bool App::submitAndPresentFrame(const SFrameSubmissionContext& frameContext) +{ + const IQueue::SSubmitInfo::SSemaphoreInfo rendered = { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1u, + .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS + }; + const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = { { .cmdbuf = frameContext.cmdbuf } }; + auto swapchainLock = m_surface->pseudoAcquire(frameContext.blitWaitValue); + const IQueue::SSubmitInfo::SSemaphoreInfo blitted = { + .semaphore = m_surface->getPresentSemaphore(), + .value = frameContext.blitWaitValue->load(), + .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS + }; + const IQueue::SSubmitInfo submitInfos[1] = { + { + .waitSemaphores = { &blitted, 1 }, + .commandBuffers = cmdbufs, + .signalSemaphores = { &rendered, 1 } + } + }; + + updateGUIDescriptorSet(); + if (getGraphicsQueue()->submit(submitInfos) != IQueue::RESULT::SUCCESS) + return false; + + ++m_realFrameIx; + const uint64_t renderedFrameIx = m_realFrameIx - 1u; + handleFrameCaptureRequests(frameContext.frame, renderedFrameIx); + + const ISmoothResizeSurface::SPresentInfo presentInfo = { + { + .source = { .image = frameContext.frame, .rect = frameContext.renderArea }, + .waitSemaphore = rendered.semaphore, + .waitValue = rendered.value, + .pPresentSemaphoreWaitValue = frameContext.blitWaitValue, + }, + frameContext.cmdbuf->getQueueFamilyIndex() + }; + m_surface->present(std::move(swapchainLock), presentInfo); + return true; +} diff --git a/61_UI/AppHeadlessCameraSmoke.cpp b/61_UI/AppHeadlessCameraSmoke.cpp new file mode 100644 index 000000000..a05de83ac --- /dev/null +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -0,0 +1,123 @@ +#include "app/App.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPathUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPersistence.hpp" +#include "nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp" +#include "nlohmann/json.hpp" +#include "AppHeadlessCameraSmokeHelpers.inl" +#include "AppHeadlessCameraSmokeChecks.inl" + +bool App::runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd_ptr&& system) +{ + auto fail = [&](const std::string& msg) -> bool + { + m_cliRuntime.headlessCameraSmokePassed = false; + return reportHeadlessCameraSmokeFailure(*this, msg); + }; + const auto runSmokeStep = [&](auto&& fn) -> bool + { + std::string smokeError; + if (fn(smokeError)) + return true; + return fail(smokeError); + }; + + if (!initializeMountedCameraResources(std::move(system))) + return fail("Failed to initialize mounted resources for headless camera smoke."); + + nbl::system::SCameraPlanarRuntimeBootstrap runtimeBootstrap = {}; + std::string jsonError; + if (!nbl::system::tryBuildCameraPlanarRuntimeBootstrap( + getCameraAppResourceContext(), + { + .requestedPath = program.is_used("--file") ? std::optional(program.get("--file")) : std::optional(std::nullopt), + .fallbackToDefault = false + }, + runtimeBootstrap, + &jsonError)) + { + return fail(jsonError); + } + + auto& cameraCollections = runtimeBootstrap.collections; + auto& cameras = cameraCollections.cameras; + auto& smokePlanars = runtimeBootstrap.planars; + + if (!runSmokeStep([&](std::string& smokeError) { return verifyScriptedRuntimeFrameBatch(&smokeError); })) + return false; + if (!runSmokeStep([&](std::string& smokeError) { return verifyScriptedRuntimeParser(&smokeError); })) + return false; + if (!runSmokeStep([&](std::string& smokeError) { return verifyScriptedCheckRunner(m_cameraGoalSolver, &smokeError); })) + return false; + + SCameraSmokePresetInventory initialPresets = {}; + if (!runSmokeStep([&](std::string& smokeError) + { + return runPerCameraPresetAndBindingSmoke(m_cameraGoalSolver, { cameras.data(), cameras.size() }, initialPresets, smokeError); + })) + { + return false; + } + + const auto cameraInventory = collectSmokeCameras({ cameras.data(), cameras.size() }); + const SCameraSmokeResolvedState resolvedSmokeState = { + .goalSolver = m_cameraGoalSolver, + .system = getCameraAppResourceContext().system, + .initialPresets = initialPresets, + .fpsCamera = cameraInventory.fps, + .orbitCamera = cameraInventory.orbit, + .arcballCamera = cameraInventory.arcball, + .turntableCamera = cameraInventory.turntable, + .topDownCamera = cameraInventory.topDown, + .isometricCamera = cameraInventory.isometric, + .freeCamera = cameraInventory.free, + .chaseCamera = cameraInventory.chase, + .dollyCamera = cameraInventory.dolly, + .pathCamera = cameraInventory.path, + .dollyZoomCamera = cameraInventory.dollyZoom + }; + + if (!runSmokeStep([&](std::string& smokeError) + { + return verifyFollowSmoke( + resolvedSmokeState, + { cameras.data(), cameras.size() }, + { smokePlanars.data(), smokePlanars.size() }, + [this](ICamera* camera) { return makeExampleDefaultFollowConfig(camera); }, + [](const CTrackedTarget& trackedTarget, const std::string_view label, std::string& error) + { + return verifyFollowTargetMarkerAlignmentForSmoke(trackedTarget, label, error); + }, + [this, smokePlanarsSpan = std::span>(smokePlanars.data(), smokePlanars.size())](ICamera* camera, const CTrackedTarget& trackedTarget, const std::string_view label, std::string& error) + { + return verifyOffsetFollowRecaptureForSmoke(m_cameraGoalSolver, smokePlanarsSpan, camera, trackedTarget, label, error); + }, + smokeError); + })) + { + return false; + } + + if (!runSmokeStep([&](std::string& smokeError) { return verifyCrossKindAndPresentationSmoke(resolvedSmokeState, smokeError); })) + return false; + if (!runSmokeStep([&](std::string& smokeError) { return verifyPersistenceAndPlaybackSmoke(resolvedSmokeState, smokeError); })) + return false; + if (!runSmokeStep([&](std::string& smokeError) { return verifySequenceCompileSmoke(resolvedSmokeState, smokeError); })) + return false; + if (!runSmokeStep([&](std::string& smokeError) { return verifyRangeAndUtilitySmoke(resolvedSmokeState, smokeError); })) + return false; + + m_cliRuntime.headlessCameraSmokePassed = true; + std::cout << "[headless-camera-smoke] PASS cameras=" << cameras.size() << std::endl; + return true; +} diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl new file mode 100644 index 000000000..730a9709a --- /dev/null +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -0,0 +1,1833 @@ + struct SCameraSmokeResolvedState final + { + const CCameraGoalSolver& goalSolver; + nbl::system::ISystem* system = nullptr; + const SCameraSmokePresetInventory& initialPresets; + ICamera* fpsCamera = nullptr; + ICamera* orbitCamera = nullptr; + ICamera* arcballCamera = nullptr; + ICamera* turntableCamera = nullptr; + ICamera* topDownCamera = nullptr; + ICamera* isometricCamera = nullptr; + ICamera* freeCamera = nullptr; + ICamera* chaseCamera = nullptr; + ICamera* dollyCamera = nullptr; + ICamera* pathCamera = nullptr; + ICamera* dollyZoomCamera = nullptr; + }; + + inline bool verifyCrossKindAndPresentationSmoke( + const SCameraSmokeResolvedState& state, + std::string& outError) + { + if (state.initialPresets.orbit.has_value() && state.initialPresets.chase.has_value()) + { + if (!verifyExactCrossKindApply(state.goalSolver, state.orbitCamera, state.initialPresets.chase.value(), "Chase->Orbit", outError)) + return false; + if (!verifyExactCrossKindApply(state.goalSolver, state.chaseCamera, state.initialPresets.orbit.value(), "Orbit->Chase", outError)) + return false; + } + + if (state.initialPresets.orbit.has_value() && state.initialPresets.dolly.has_value()) + { + if (!verifyExactCrossKindApply(state.goalSolver, state.orbitCamera, state.initialPresets.dolly.value(), "Dolly->Orbit", outError)) + return false; + if (!verifyExactCrossKindApply(state.goalSolver, state.dollyCamera, state.initialPresets.orbit.value(), "Orbit->Dolly", outError)) + return false; + } + + if (state.initialPresets.orbit.has_value() && state.initialPresets.path.has_value() && state.orbitCamera) + { + if (!verifyApproximateCrossKindApply( + state.goalSolver, + state.orbitCamera, + state.initialPresets.path.value(), + CCameraGoalSolver::SApplyResult::EIssue::MissingPathState, + "Path->Orbit", + outError)) + { + return false; + } + } + + if (state.initialPresets.orbit.has_value() && state.initialPresets.dollyZoom.has_value() && state.orbitCamera) + { + if (!verifyApproximateCrossKindApply( + state.goalSolver, + state.orbitCamera, + state.initialPresets.dollyZoom.value(), + CCameraGoalSolver::SApplyResult::EIssue::MissingDynamicPerspectiveState, + "DollyZoom->Orbit", + outError)) + { + return false; + } + } + + if (!state.initialPresets.orbit.has_value()) + return true; + + if (std::string_view(nbl::ui::CCameraPresentationUtilities::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || + std::string_view(nbl::ui::CCameraPresentationUtilities::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || + std::string_view(nbl::ui::CCameraPresentationUtilities::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") + { + outError = "Presentation utilities smoke returned an unexpected filter label."; + return false; + } + + const auto blockedPresentation = nbl::ui::CCameraPresentationUtilities::analyzePresetPresentation(state.goalSolver, nullptr, state.initialPresets.orbit.value()); + if (blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || + blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) + { + outError = "Presentation utilities smoke allowed a null-camera preset through an exactness filter."; + return false; + } + if (blockedPresentation.sourceKindLabel.empty() || blockedPresentation.goalStateLabel.empty()) + { + outError = "Presentation utilities smoke produced empty blocked presentation labels."; + return false; + } + + const auto blockedBadges = nbl::ui::CCameraPresentationUtilities::collectGoalApplyPresentationBadges(blockedPresentation); + if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort || blockedPresentation.badges.blocked != blockedBadges.blocked) + { + outError = "Presentation utilities smoke produced wrong blocked badge flags."; + return false; + } + + if (state.orbitCamera) + { + const auto exactPresentation = nbl::ui::CCameraPresentationUtilities::analyzePresetPresentation(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value()); + if (!exactPresentation.matchesFilter(EPresetApplyPresentationFilter::All) || + !exactPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || + exactPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) + { + outError = "Presentation utilities smoke failed exact filtering."; + return false; + } + + const auto exactBadges = nbl::ui::CCameraPresentationUtilities::collectGoalApplyPresentationBadges(exactPresentation); + if (!exactBadges.exact || exactBadges.bestEffort || exactBadges.dropsState || exactBadges.sharedStateOnly || exactBadges.blocked) + { + outError = "Presentation utilities smoke produced wrong exact badge flags."; + return false; + } + if (exactPresentation.sourceKindLabel.empty() || exactPresentation.goalStateLabel.empty()) + { + outError = "Presentation utilities smoke produced empty exact presentation labels."; + return false; + } + + const auto capturePresentation = nbl::ui::CCameraPresentationUtilities::analyzeCapturePresentation(state.goalSolver, state.orbitCamera); + if (!capturePresentation.canCapture || capturePresentation.policyLabel.empty()) + { + outError = "Presentation utilities smoke failed orbit capture presentation."; + return false; + } + } + + if (state.initialPresets.path.has_value() && state.orbitCamera) + { + const auto approximatePresentation = nbl::ui::CCameraPresentationUtilities::analyzePresetPresentation(state.goalSolver, state.orbitCamera, state.initialPresets.path.value()); + if (!approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::All) || + approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || + !approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) + { + outError = "Presentation utilities smoke failed best-effort filtering."; + return false; + } + + const auto approximateBadges = nbl::ui::CCameraPresentationUtilities::collectGoalApplyPresentationBadges(approximatePresentation); + if (approximateBadges.exact || !approximateBadges.bestEffort || !approximateBadges.dropsState || approximateBadges.sharedStateOnly || approximateBadges.blocked) + { + outError = "Presentation utilities smoke produced wrong best-effort badge flags."; + return false; + } + if (approximatePresentation.sourceKindLabel.empty() || approximatePresentation.goalStateLabel.empty()) + { + outError = "Presentation utilities smoke produced empty best-effort presentation labels."; + return false; + } + } + + return true; + } + + inline std::vector collectAvailableSmokePresets(const SCameraSmokePresetInventory& initialPresets) + { + std::vector sourcePresets; + sourcePresets.reserve(5u); + if (initialPresets.orbit.has_value()) + sourcePresets.push_back(initialPresets.orbit.value()); + if (initialPresets.chase.has_value()) + sourcePresets.push_back(initialPresets.chase.value()); + if (initialPresets.dolly.has_value()) + sourcePresets.push_back(initialPresets.dolly.value()); + if (initialPresets.path.has_value()) + sourcePresets.push_back(initialPresets.path.value()); + if (initialPresets.dollyZoom.has_value()) + sourcePresets.push_back(initialPresets.dollyZoom.value()); + return sourcePresets; + } + + inline float chooseShiftedReferenceDistance(const ICamera::SphericalTargetState& state) + { + const float farther = std::min(state.maxDistance, state.distance + 1.25f); + if (hlsl::abs(static_cast(farther - state.distance)) > CameraTinyScalarEpsilon) + return farther; + + const float nearer = std::max(state.minDistance, state.distance - 1.25f); + return nearer; + } + + inline bool tryBuildReferenceFrameFromTargetRelativeState( + const nbl::core::SCameraTargetRelativeState& desiredState, + hlsl::float64_t4x4& outReferenceFrame, + nbl::core::CCameraGoal& outExpectedGoal) + { + outExpectedGoal = {}; + nbl::core::SCameraTargetRelativePose pose = {}; + if (!nbl::core::CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState( + desiredState, + nbl::core::SCameraTargetRelativeTraits::MinDistance, + nbl::core::SCameraTargetRelativeTraits::DefaultMaxDistance, + pose) || + !nbl::core::CCameraGoalUtilities::applyCanonicalTargetRelativeGoal(outExpectedGoal, desiredState)) + { + return false; + } + + outReferenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(pose.position, pose.orientation); + return true; + } + + inline bool verifyReferenceFrameGoalApply( + const SCameraSmokeResolvedState& state, + ICamera* const camera, + const nbl::core::SCameraTargetRelativeState& desiredState, + std::string_view label, + std::string& outError) + { + ICamera::SphericalTargetState baselineState = {}; + if (!camera->tryGetSphericalTargetState(baselineState)) + { + outError = std::string(label) + " reference-frame smoke failed to capture the baseline spherical state."; + return false; + } + + const nbl::core::SCameraTargetRelativeState baselineTargetRelativeState = { + .target = baselineState.target, + .orbitUv = baselineState.orbitUv, + .distance = baselineState.distance + }; + + hlsl::float64_t4x4 referenceFrame = hlsl::float64_t4x4(1.0); + hlsl::float64_t4x4 baselineReferenceFrame = hlsl::float64_t4x4(1.0); + nbl::core::CCameraGoal expectedGoal = {}; + nbl::core::CCameraGoal baselineGoal = {}; + if (!tryBuildReferenceFrameFromTargetRelativeState(desiredState, referenceFrame, expectedGoal) || + !tryBuildReferenceFrameFromTargetRelativeState(baselineTargetRelativeState, baselineReferenceFrame, baselineGoal)) + { + outError = std::string(label) + " reference-frame smoke failed to build the projected reference pose."; + return false; + } + + if (!camera->manipulate({}, &referenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to apply the reference pose through manipulate({}, &referenceFrame)."; + return false; + } + + ICamera::SphericalTargetState actualState = {}; + if (!camera->tryGetSphericalTargetState(actualState) || + !hlsl::CCameraMathUtilities::nearlyEqualVec3( + actualState.target, + desiredState.target, + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance) || + hlsl::CCameraMathUtilities::getWrappedAngleDistanceRadians(actualState.orbitUv.x, desiredState.orbitUv.x) > + hlsl::radians(nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg) || + hlsl::CCameraMathUtilities::getWrappedAngleDistanceRadians(actualState.orbitUv.y, desiredState.orbitUv.y) > + hlsl::radians(nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg) || + hlsl::abs(static_cast(actualState.distance - desiredState.distance)) > + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance) + { + std::ostringstream oss; + oss << label + << " reference-frame smoke produced the wrong spherical state:" + << " actual_target=(" << actualState.target.x << "," << actualState.target.y << "," << actualState.target.z << ")" + << " expected_target=(" << desiredState.target.x << "," << desiredState.target.y << "," << desiredState.target.z << ")" + << " actual_orbit=(" << actualState.orbitUv.x << "," << actualState.orbitUv.y << ")" + << " expected_orbit=(" << desiredState.orbitUv.x << "," << desiredState.orbitUv.y << ")" + << " actual_distance=" << actualState.distance + << " expected_distance=" << desiredState.distance; + outError = oss.str(); + return false; + } + + expectedGoal.hasTargetPosition = false; + expectedGoal.hasDistance = false; + expectedGoal.hasOrbitState = false; + + const auto capture = state.goalSolver.captureDetailed(camera); + if (!capture.canUseGoal() || + !nbl::core::CCameraGoalUtilities::compareGoals( + capture.goal, + expectedGoal, + nbl::system::SCameraSmokeComparisonThresholds::StrictPositionTolerance, + nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance)) + { + outError = std::string(label) + " reference-frame smoke produced the wrong projected goal: " + + (capture.canUseGoal() ? nbl::core::CCameraGoalUtilities::describeGoalMismatch(capture.goal, expectedGoal) : std::string("goal_state=unavailable")); + return false; + } + + if (!camera->manipulate({}, &baselineReferenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to restore the baseline reference pose through manipulate({}, &referenceFrame)."; + return false; + } + + return true; + } + + inline bool verifyReferenceFramePoseApply( + const SCameraSmokeResolvedState& state, + ICamera* const camera, + const hlsl::float64_t3& desiredPosition, + const hlsl::camera_quaternion_t& desiredOrientation, + std::string_view label, + std::string& outError) + { + const auto baselineCapture = state.goalSolver.captureDetailed(camera); + if (!baselineCapture.canUseGoal()) + { + outError = std::string(label) + " reference-frame smoke failed to capture the baseline pose."; + return false; + } + + nbl::core::CCameraGoal expectedGoal = {}; + expectedGoal.position = desiredPosition; + expectedGoal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(desiredOrientation); + + const auto baselineReferenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix( + baselineCapture.goal.position, + baselineCapture.goal.orientation); + const auto referenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix( + desiredPosition, + expectedGoal.orientation); + if (!camera->manipulate({}, &referenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to apply the rigid reference pose through manipulate({}, &referenceFrame)."; + return false; + } + + const auto capture = state.goalSolver.captureDetailed(camera); + if (!capture.canUseGoal() || + !nbl::core::CCameraGoalUtilities::compareGoals( + capture.goal, + expectedGoal, + nbl::system::SCameraSmokeComparisonThresholds::StrictPositionTolerance, + nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance)) + { + outError = std::string(label) + " reference-frame smoke produced the wrong rigid goal: " + + (capture.canUseGoal() ? nbl::core::CCameraGoalUtilities::describeGoalMismatch(capture.goal, expectedGoal) : std::string("goal_state=unavailable")); + return false; + } + + if (!camera->manipulate({}, &baselineReferenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to restore the baseline rigid pose through manipulate({}, &referenceFrame)."; + return false; + } + + return true; + } + + inline bool verifyReferenceFrameSupportSmoke( + const SCameraSmokeResolvedState& state, + std::string& outError) + { + if (state.fpsCamera) + { + if (!verifyReferenceFramePoseApply( + state, + state.fpsCamera, + hlsl::float64_t3(2.5, -0.75, 4.0), + hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::float64_t3(-20.0, 35.0, 0.0)), + "FPS", + outError)) + { + return false; + } + } + + if (state.freeCamera) + { + if (!verifyReferenceFramePoseApply( + state, + state.freeCamera, + hlsl::float64_t3(-1.25, 0.5, 3.5), + hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::float64_t3(15.0, 45.0, 20.0)), + "Free", + outError)) + { + return false; + } + } + + const auto verifySphericalReference = [&](ICamera* const camera, std::string_view label, const auto& mutateDesiredState) -> bool + { + if (!camera) + return true; + + ICamera::SphericalTargetState baselineState = {}; + if (!camera->tryGetSphericalTargetState(baselineState)) + { + outError = std::string(label) + " reference-frame smoke failed to query the baseline spherical state."; + return false; + } + + nbl::core::SCameraTargetRelativeState desiredState = { + .target = baselineState.target, + .orbitUv = baselineState.orbitUv, + .distance = chooseShiftedReferenceDistance(baselineState) + }; + mutateDesiredState(desiredState); + return verifyReferenceFrameGoalApply(state, camera, desiredState, label, outError); + }; + + if (!verifySphericalReference(state.orbitCamera, "Orbit", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv += hlsl::float64_t2(0.45, -0.25); + })) + { + return false; + } + + if (!verifySphericalReference(state.arcballCamera, "Arcball", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv += hlsl::float64_t2(0.35, 0.2); + desiredState.orbitUv.y = std::clamp( + desiredState.orbitUv.y, + -static_cast(nbl::core::SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad), + static_cast(nbl::core::SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad)); + })) + { + return false; + } + + if (!verifySphericalReference(state.turntableCamera, "Turntable", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv += hlsl::float64_t2(-0.4, 0.18); + desiredState.orbitUv.y = std::clamp( + desiredState.orbitUv.y, + -static_cast(nbl::core::SCameraTargetRelativeRigDefaults::TurntablePitchLimitRad), + static_cast(nbl::core::SCameraTargetRelativeRigDefaults::TurntablePitchLimitRad)); + })) + { + return false; + } + + if (!verifySphericalReference(state.topDownCamera, "TopDown", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv = hlsl::float64_t2( + desiredState.orbitUv.x + 0.6, + nbl::core::SCameraTargetRelativeRigDefaults::TopDownPitchRad); + })) + { + return false; + } + + if (!verifySphericalReference(state.isometricCamera, "Isometric", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv = hlsl::float64_t2( + nbl::core::SCameraTargetRelativeRigDefaults::IsometricYawRad, + nbl::core::SCameraTargetRelativeRigDefaults::IsometricPitchRad); + })) + { + return false; + } + + if (!verifySphericalReference(state.chaseCamera, "Chase", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv += hlsl::float64_t2(0.3, 0.15); + desiredState.orbitUv.y = std::clamp( + desiredState.orbitUv.y, + static_cast(nbl::core::SCameraTargetRelativeRigDefaults::ChaseMinPitchRad), + static_cast(nbl::core::SCameraTargetRelativeRigDefaults::ChaseMaxPitchRad)); + })) + { + return false; + } + + if (!verifySphericalReference(state.dollyCamera, "Dolly", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv += hlsl::float64_t2(-0.3, -0.22); + desiredState.orbitUv.y = std::clamp( + desiredState.orbitUv.y, + -static_cast(nbl::core::SCameraTargetRelativeRigDefaults::DollyPitchLimitRad), + static_cast(nbl::core::SCameraTargetRelativeRigDefaults::DollyPitchLimitRad)); + })) + { + return false; + } + + if (!verifySphericalReference(state.dollyZoomCamera, "DollyZoom", [&](nbl::core::SCameraTargetRelativeState& desiredState) + { + desiredState.orbitUv += hlsl::float64_t2(0.28, -0.14); + })) + { + return false; + } + + if (state.pathCamera) + { + ICamera::PathState baselinePathState = {}; + ICamera::PathStateLimits pathLimits = {}; + ICamera::SphericalTargetState sphericalState = {}; + if (!state.pathCamera->tryGetPathState(baselinePathState) || + !state.pathCamera->tryGetPathStateLimits(pathLimits) || + !state.pathCamera->tryGetSphericalTargetState(sphericalState)) + { + outError = "Path reference-frame smoke failed to query the baseline typed state."; + return false; + } + + ICamera::PathState desiredPathState = {}; + ICamera::PathState projectedPathState = {}; + const auto pathDelta = nbl::core::CCameraPathUtilities::makePathDeltaFromVirtualPathMotion( + hlsl::float64_t3(0.8, 0.35, 1.1), + hlsl::float64_t3(0.0, 0.0, 0.45)); + if (!nbl::core::CCameraPathUtilities::tryApplyPathStateDelta( + baselinePathState, + pathDelta, + pathLimits, + desiredPathState)) + { + outError = "Path reference-frame smoke failed to build the desired typed path state."; + return false; + } + + nbl::core::SCameraCanonicalPathState canonicalPathState = {}; + nbl::core::SCameraCanonicalPathState baselineCanonicalPathState = {}; + nbl::core::CCameraGoal expectedGoal = {}; + if (!nbl::core::CCameraPathUtilities::tryBuildCanonicalPathState( + sphericalState.target, + desiredPathState, + pathLimits, + canonicalPathState) || + !nbl::core::CCameraPathUtilities::tryResolvePathState( + sphericalState.target, + canonicalPathState.pose.position, + pathLimits, + nullptr, + projectedPathState) || + !nbl::core::CCameraPathUtilities::tryBuildCanonicalPathState( + sphericalState.target, + baselinePathState, + pathLimits, + baselineCanonicalPathState) || + !nbl::core::CCameraGoalUtilities::applyCanonicalPathGoalFields( + expectedGoal, + sphericalState.target, + projectedPathState, + pathLimits)) + { + outError = "Path reference-frame smoke failed to build the canonical target-relative path pose."; + return false; + } + + const auto baselineReferenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix( + baselineCanonicalPathState.pose.position, + baselineCanonicalPathState.pose.orientation); + const auto referenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix( + canonicalPathState.pose.position, + canonicalPathState.pose.orientation); + if (!state.pathCamera->manipulate({}, &referenceFrame)) + { + outError = "Path reference-frame smoke failed to apply the projected path pose through manipulate({}, &referenceFrame)."; + return false; + } + + const auto capture = state.goalSolver.captureDetailed(state.pathCamera); + if (!capture.canUseGoal() || + !nbl::core::CCameraGoalUtilities::compareGoals( + capture.goal, + expectedGoal, + nbl::system::SCameraSmokeComparisonThresholds::StrictPositionTolerance, + nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance)) + { + outError = "Path reference-frame smoke produced the wrong projected goal: " + + (capture.canUseGoal() ? nbl::core::CCameraGoalUtilities::describeGoalMismatch(capture.goal, expectedGoal) : std::string("goal_state=unavailable")); + return false; + } + + if (!state.pathCamera->manipulate({}, &baselineReferenceFrame)) + { + outError = "Path reference-frame smoke failed to restore the baseline reference pose through manipulate({}, &referenceFrame)."; + return false; + } + } + + return true; + } + + inline bool verifyPersistenceAndPlaybackSmoke( + const SCameraSmokeResolvedState& state, + std::string& outError) + { + auto sourcePresets = collectAvailableSmokePresets(state.initialPresets); + if (sourcePresets.empty()) + { + outError = "Preset persistence smoke failed to collect source presets."; + return false; + } + + const auto sourcePresetSpan = std::span(sourcePresets.data(), sourcePresets.size()); + + const auto presetText = nbl::system::CCameraPersistenceUtilities::serializePresetCollection(sourcePresetSpan); + if (presetText.empty()) + { + outError = "Preset persistence smoke failed to serialize preset collection."; + return false; + } + + std::vector loadedPresets; + if (!nbl::system::CCameraPersistenceUtilities::deserializePresetCollection(presetText, loadedPresets)) + { + outError = "Preset persistence smoke failed to deserialize preset collection."; + return false; + } + if (!nbl::core::CCameraPresetUtilities::comparePresetCollections( + sourcePresetSpan, + std::span(loadedPresets.data(), loadedPresets.size()), + SCameraSmokePersistenceThresholds::PositionTolerance, + SCameraSmokePersistenceThresholds::AngularToleranceDeg, + SCameraSmokePersistenceThresholds::ScalarTolerance)) + { + outError = "Preset persistence smoke changed stream preset collection content."; + return false; + } + + CCameraKeyframeTrack sourceTrack; + sourceTrack.keyframes.reserve(sourcePresets.size()); + for (size_t i = 0u; i < sourcePresets.size(); ++i) + { + nbl::core::CCameraKeyframe keyframe; + keyframe.time = static_cast(i) * 1.5f; + keyframe.preset = sourcePresets[i]; + sourceTrack.keyframes.emplace_back(std::move(keyframe)); + } + sourceTrack.selectedKeyframeIx = static_cast(sourceTrack.keyframes.size()) - 1; + + const auto keyframeText = nbl::system::CCameraKeyframeTrackPersistenceUtilities::serializeKeyframeTrack(sourceTrack); + if (keyframeText.empty()) + { + outError = "Keyframe persistence smoke failed to serialize track."; + return false; + } + + CCameraKeyframeTrack loadedTrack; + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::deserializeKeyframeTrack(keyframeText, loadedTrack)) + { + outError = "Keyframe persistence smoke failed to deserialize track."; + return false; + } + if (!nbl::system::CCameraSmokeRegressionUtilities::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, loadedTrack)) + { + outError = "Keyframe persistence smoke changed stream track content."; + return false; + } + + struct TempFileCleanup final + { + std::vector paths; + + ~TempFileCleanup() + { + std::error_code ec; + for (const auto& path : paths) + std::filesystem::remove(path, ec); + } + } tempFiles; + + const auto uniqueSuffix = std::to_string(static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto tempDir = std::filesystem::temp_directory_path(); + const auto presetFile = tempDir / ("nabla_cameraz_presets_" + uniqueSuffix + ".json"); + const auto keyframeFile = tempDir / ("nabla_cameraz_keyframes_" + uniqueSuffix + ".json"); + tempFiles.paths = { presetFile, keyframeFile }; + + if (!state.system) + { + outError = "Persistence smoke is missing a valid system interface."; + return false; + } + + auto& system = *state.system; + + if (!nbl::system::CCameraPersistenceUtilities::savePresetCollectionToFile(system, presetFile, sourcePresetSpan)) + { + outError = "Preset persistence smoke failed to save preset collection file."; + return false; + } + + std::vector fileLoadedPresets; + if (!nbl::system::CCameraPersistenceUtilities::loadPresetCollectionFromFile(system, presetFile, fileLoadedPresets)) + { + outError = "Preset persistence smoke failed to load preset collection file."; + return false; + } + if (!nbl::core::CCameraPresetUtilities::comparePresetCollections( + sourcePresetSpan, + std::span(fileLoadedPresets.data(), fileLoadedPresets.size()), + SCameraSmokePersistenceThresholds::PositionTolerance, + SCameraSmokePersistenceThresholds::AngularToleranceDeg, + SCameraSmokePersistenceThresholds::ScalarTolerance)) + { + outError = "Preset persistence smoke changed file preset collection content."; + return false; + } + + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::saveKeyframeTrackToFile(system, keyframeFile, sourceTrack)) + { + outError = "Keyframe persistence smoke failed to save track file."; + return false; + } + + CCameraKeyframeTrack fileLoadedTrack; + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::loadKeyframeTrackFromFile(system, keyframeFile, fileLoadedTrack)) + { + outError = "Keyframe persistence smoke failed to load track file."; + return false; + } + if (!nbl::system::CCameraSmokeRegressionUtilities::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, fileLoadedTrack)) + { + outError = "Keyframe persistence smoke changed file track content."; + return false; + } + + if (state.initialPresets.orbit.has_value() && state.initialPresets.dolly.has_value()) + { + CCameraKeyframeTrack playbackTrack; + { + nbl::core::CCameraKeyframe keyframe; + keyframe.time = 0.f; + keyframe.preset = state.initialPresets.orbit.value(); + playbackTrack.keyframes.push_back(keyframe); + } + { + nbl::core::CCameraKeyframe keyframe; + keyframe.time = SCameraSmokePlaybackDefaults::EndKeyframeTime; + keyframe.preset = state.initialPresets.dolly.value(); + playbackTrack.keyframes.push_back(keyframe); + } + + CCameraPlaybackCursor cursor = { + .playing = true, + .loop = false, + .speed = 1.f, + .time = SCameraSmokePlaybackDefaults::MidPlaybackTime + }; + + const auto advanceToEnd = nbl::core::CCameraPlaybackTimelineUtilities::advancePlaybackCursor(cursor, playbackTrack, SCameraSmokePlaybackDefaults::AdvanceDt); + if (!advanceToEnd.hasTrack || !advanceToEnd.changedTime || !advanceToEnd.reachedEnd || advanceToEnd.wrapped || !advanceToEnd.stopped) + { + outError = "Playback timeline smoke failed for non-loop end-of-track advance."; + return false; + } + if (hlsl::abs(static_cast(advanceToEnd.time - SCameraSmokePlaybackDefaults::EndKeyframeTime)) > CameraTinyScalarEpsilon) + { + outError = "Playback timeline smoke produced wrong end-of-track time."; + return false; + } + + nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(cursor, SCameraSmokePlaybackDefaults::ResetPlaybackTime); + if (cursor.playing || hlsl::abs(static_cast(cursor.time - SCameraSmokePlaybackDefaults::ResetPlaybackTime)) > CameraTinyScalarEpsilon) + { + outError = "Playback timeline smoke failed to reset cursor."; + return false; + } + + cursor.playing = true; + cursor.loop = true; + cursor.speed = 1.f; + cursor.time = SCameraSmokePlaybackDefaults::MidPlaybackTime; + const auto advanceLoop = nbl::core::CCameraPlaybackTimelineUtilities::advancePlaybackCursor(cursor, playbackTrack, SCameraSmokePlaybackDefaults::AdvanceDt); + if (!advanceLoop.hasTrack || !advanceLoop.changedTime || !advanceLoop.wrapped || advanceLoop.stopped || advanceLoop.reachedEnd) + { + outError = "Playback timeline smoke failed for looped advance."; + return false; + } + if (hlsl::abs(static_cast(advanceLoop.time - SCameraSmokePlaybackDefaults::WrappedPlaybackTime)) > CameraTinyScalarEpsilon) + { + outError = "Playback timeline smoke produced wrong wrapped time."; + return false; + } + + cursor.time = SCameraSmokePlaybackDefaults::OvershootPlaybackTime; + nbl::core::CCameraPlaybackTimelineUtilities::clampPlaybackCursorToTrack(playbackTrack, cursor); + if (hlsl::abs(static_cast(cursor.time - SCameraSmokePlaybackDefaults::EndKeyframeTime)) > CameraTinyScalarEpsilon) + { + outError = "Playback timeline smoke failed to clamp cursor time."; + return false; + } + } + + return true; + } + + inline bool verifySequenceCompileSmoke( + const SCameraSmokeResolvedState& state, + std::string& outError) + { + if (!state.initialPresets.orbit.has_value()) + return true; + + CCameraSequenceScript sequence; + sequence.fps = SCameraSmokeSequenceDefaults::Fps; + sequence.defaults.durationSeconds = SCameraSmokeSequenceDefaults::DurationSeconds; + sequence.defaults.presentations = { + { .projection = IPlanarProjection::CProjection::Perspective, .leftHanded = true }, + { .projection = IPlanarProjection::CProjection::Orthographic, .leftHanded = false } + }; + sequence.defaults.captureFractions = { SCameraSmokeSequenceDefaults::CaptureFractions[0], SCameraSmokeSequenceDefaults::CaptureFractions[1], SCameraSmokeSequenceDefaults::CaptureFractions[2] }; + + CCameraSequenceSegment segment; + segment.name = "sequence_compile_smoke"; + segment.cameraKind = ICamera::CameraKind::Orbit; + { + CCameraSequenceKeyframe keyframe; + keyframe.time = 0.f; + keyframe.hasAbsolutePreset = true; + keyframe.absolutePreset = state.initialPresets.orbit.value(); + segment.keyframes.push_back(keyframe); + } + for (const auto& [time, position] : { + std::pair{ 0.0f, SCameraSmokeSequenceDefaults::TargetPositionA }, + std::pair{ SCameraSmokeSequenceDefaults::SecondKeyframeTime, SCameraSmokeSequenceDefaults::TargetPositionB }, + std::pair{ SCameraSmokeSequenceDefaults::SecondKeyframeTime, SCameraSmokeSequenceDefaults::TargetPositionC } }) + { + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; + keyframe.time = time; + keyframe.hasAbsolutePosition = true; + keyframe.absolutePosition = position; + segment.targetKeyframes.push_back(keyframe); + } + sequence.segments.push_back(segment); + + if (!nbl::core::CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(sequence)) + { + outError = "Sequence compile smoke failed to detect multi-presentation authored defaults."; + return false; + } + + CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = {}; + referenceTrackedTargetPose.position = SCameraAppSceneDefaults::DefaultFollowTargetPosition; + referenceTrackedTargetPose.orientation = SCameraAppSceneDefaults::DefaultFollowTargetOrientation; + + nbl::core::CCameraSequenceCompiledSegment compiledSegment; + std::string compileError; + if (!nbl::core::CCameraSequenceScriptUtilities::compileSequenceSegmentFromReference( + sequence, + sequence.segments.front(), + state.initialPresets.orbit.value(), + referenceTrackedTargetPose, + compiledSegment, + &compileError)) + { + outError = "Sequence compile smoke failed to compile a shared segment. " + compileError; + return false; + } + + if (compiledSegment.durationFrames != SCameraSmokeSequenceDefaults::DurationFrames || + compiledSegment.sampleTimes.size() != SCameraSmokeSequenceDefaults::DurationFrames) + { + outError = "Sequence compile smoke produced wrong sampled frame count."; + return false; + } + if (compiledSegment.captureFrameOffsets != std::vector( + SCameraSmokeSequenceDefaults::CaptureFrameOffsets.begin(), + SCameraSmokeSequenceDefaults::CaptureFrameOffsets.end())) + { + outError = "Sequence compile smoke produced wrong capture frame offsets."; + return false; + } + if (compiledSegment.presentations.size() != 2u) + { + outError = "Sequence compile smoke lost authored presentations."; + return false; + } + if (!compiledSegment.usesTrackedTargetTrack() || compiledSegment.trackedTargetTrack.keyframes.size() != 2u) + { + outError = "Sequence compile smoke failed to normalize tracked-target keyframes."; + return false; + } + + std::vector framePolicies; + if (!nbl::core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) + { + outError = "Sequence compile smoke failed to build shared frame policies."; + return false; + } + if (framePolicies.size() != SCameraSmokeSequenceDefaults::DurationFrames) + { + outError = "Sequence compile smoke produced wrong frame-policy count."; + return false; + } + if (!framePolicies[0].baseline || framePolicies[0].continuityStep || !framePolicies[0].capture) + { + outError = "Sequence compile smoke produced wrong first-frame policy."; + return false; + } + if (!framePolicies[1].continuityStep || !framePolicies[1].followTargetLock || framePolicies[1].baseline) + { + outError = "Sequence compile smoke produced wrong continuity follow policy."; + return false; + } + if (!framePolicies[4].capture || !framePolicies[7].capture) + { + outError = "Sequence compile smoke produced wrong capture milestone policy."; + return false; + } + + CCameraSequenceTrackedTargetPose poseAtOne; + if (!nbl::core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) + { + outError = "Sequence compile smoke failed to sample normalized tracked-target track."; + return false; + } + if (length(poseAtOne.position - SCameraSmokeSequenceDefaults::TargetPositionC) > CameraTinyScalarEpsilon) + { + outError = "Sequence compile smoke did not keep the last authored target pose for duplicate keyframe time."; + return false; + } + + CCameraScriptedTimeline scriptedTimeline; + std::vector actionEvents; + std::string runtimeBuildError; + if (!nbl::this_example::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( + scriptedTimeline, + actionEvents, + SCameraSmokeSequenceDefaults::StartFrame, + compiledSegment, + { + .planarIx = SCameraSmokeSequenceDefaults::PlanarIx, + .availableWindowCount = SCameraSmokeSequenceDefaults::AvailableWindowCount, + .useWindow = true, + .includeFollowTargetLock = true + }, + &runtimeBuildError)) + { + outError = "Sequence runtime builder smoke failed to append a compiled segment. " + runtimeBuildError; + return false; + } + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(scriptedTimeline); + nbl::this_example::CCameraScriptedActionUtilities::finalizeActionEvents(actionEvents); + + if (scriptedTimeline.captureFrames != std::vector( + SCameraSmokeSequenceDefaults::CaptureFrames.begin(), + SCameraSmokeSequenceDefaults::CaptureFrames.end())) + { + outError = "Sequence runtime builder smoke produced wrong capture frames."; + return false; + } + + size_t baselineChecks = 0u; + size_t stepChecks = 0u; + size_t followChecks = 0u; + for (const auto& check : scriptedTimeline.checks) + { + switch (check.kind) + { + case CCameraScriptedInputCheck::Kind::Baseline: + ++baselineChecks; + break; + case CCameraScriptedInputCheck::Kind::GimbalStep: + ++stepChecks; + break; + case CCameraScriptedInputCheck::Kind::FollowTargetLock: + ++followChecks; + break; + default: + break; + } + } + if (baselineChecks != SCameraSmokeSequenceDefaults::BaselineCheckCount || + stepChecks != SCameraSmokeSequenceDefaults::ContinuityCheckCount || + followChecks != SCameraSmokeSequenceDefaults::FollowCheckCount) + { + outError = "Sequence runtime builder smoke produced wrong scripted check counts."; + return false; + } + + size_t runtimeNextEventIndex = 0u; + size_t runtimeNextActionIndex = 0u; + CCameraScriptedFrameEvents runtimeBatch; + std::vector runtimeActions; + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, SCameraSmokeSequenceDefaults::StartFrame, runtimeBatch); + nbl::this_example::CCameraScriptedActionUtilities::dequeueFrameActions(actionEvents, runtimeNextActionIndex, SCameraSmokeSequenceDefaults::StartFrame, runtimeActions); + if (runtimeActions.size() != 10u || runtimeBatch.goals.size() != 1u || + runtimeBatch.trackedTargetTransforms.size() != 1u || runtimeBatch.segmentLabels.size() != 1u) + { + outError = "Sequence runtime builder smoke produced wrong first-frame batch."; + return false; + } + if (!nbl::this_example::CCameraScriptedActionUtilities::hasCode(runtimeActions.front(), nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow) || + runtimeBatch.segmentLabels.front() != "sequence_compile_smoke") + { + outError = "Sequence runtime builder smoke lost first-frame scripted payload."; + return false; + } + + return true; + } + + inline bool verifyRangeAndUtilitySmoke( + const SCameraSmokeResolvedState& state, + std::string& outError) + { + if (state.initialPresets.orbit.has_value() && state.orbitCamera) + { + std::array exactTargets = { state.orbitCamera, nullptr }; + const auto exactSummary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( + state.goalSolver, + std::span(exactTargets.data(), exactTargets.size()), + state.initialPresets.orbit.value()); + if (exactSummary.targetCount != 1u || exactSummary.successCount != 1u || exactSummary.approximateCount != 0u || exactSummary.failureCount != 0u) + { + outError = "Preset apply summary smoke failed for exact target range."; + return false; + } + } + + if (state.initialPresets.path.has_value() && state.orbitCamera) + { + std::array approximateTargets = { state.orbitCamera }; + const auto approximateSummary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( + state.goalSolver, + std::span(approximateTargets.data(), approximateTargets.size()), + state.initialPresets.path.value()); + if (approximateSummary.targetCount != 1u || approximateSummary.successCount != 1u || approximateSummary.approximateCount != 1u || approximateSummary.failureCount != 0u) + { + outError = "Preset apply summary smoke failed for approximate target range."; + return false; + } + } + + if (state.initialPresets.path.has_value() && state.pathCamera) + { + if (!restorePresetStrict( + state.goalSolver, + state.pathCamera, + state.initialPresets.path.value(), + "Path manipulation smoke failed to restore the baseline preset", + outError)) + { + return false; + } + + ICamera::PathState baselinePathState = {}; + if (!state.pathCamera->tryGetPathState(baselinePathState)) + { + outError = "Path manipulation smoke failed to read the baseline path state."; + return false; + } + + const hlsl::float64_t3 directTranslationMagnitude(1.5, 0.75, 2.0); + const double directRollMagnitude = 0.5; + const std::array directPathEvents = {{ + { CVirtualGimbalEvent::MoveRight, directTranslationMagnitude.x }, + { CVirtualGimbalEvent::MoveUp, directTranslationMagnitude.y }, + { CVirtualGimbalEvent::MoveForward, directTranslationMagnitude.z }, + { CVirtualGimbalEvent::RollRight, directRollMagnitude } + }}; + + if (!state.pathCamera->manipulate({ directPathEvents.data(), directPathEvents.size() })) + { + outError = "Path manipulation smoke failed to apply direct path virtual events."; + return false; + } + + ICamera::PathState manipulatedPathState = {}; + if (!state.pathCamera->tryGetPathState(manipulatedPathState)) + { + outError = "Path manipulation smoke failed to read the manipulated path state."; + return false; + } + + ICamera::PathStateLimits activePathLimits = nbl::core::CCameraPathUtilities::makeDefaultPathLimits(); + state.pathCamera->tryGetPathStateLimits(activePathLimits); + const auto expectedPathDelta = nbl::core::CCameraPathUtilities::makePathDeltaFromVirtualPathMotion( + state.pathCamera->scaleVirtualTranslation(directTranslationMagnitude), + state.pathCamera->scaleVirtualRotation(hlsl::float64_t3(0.0, 0.0, directRollMagnitude))); + ICamera::PathState expectedPathState = {}; + if (!nbl::core::CCameraPathUtilities::tryApplyPathStateDelta( + baselinePathState, + expectedPathDelta, + activePathLimits, + expectedPathState) || + !nbl::core::CCameraPathUtilities::pathStatesNearlyEqual( + manipulatedPathState, + expectedPathState, + nbl::core::SCameraPathDefaults::ExactComparisonThresholds)) + { + outError = "Path manipulation smoke changed the default s/u/v/roll runtime mapping."; + return false; + } + + const auto movedCapture = state.goalSolver.captureDetailed(state.pathCamera); + if (!movedCapture.canUseGoal()) + { + outError = "Path manipulation smoke failed to capture the moved path goal."; + return false; + } + + if (!restorePresetStrict( + state.goalSolver, + state.pathCamera, + state.initialPresets.path.value(), + "Path manipulation smoke failed to reset the baseline preset before replay", + outError)) + { + return false; + } + + std::vector replayEvents; + if (!state.goalSolver.buildEvents(state.pathCamera, movedCapture.goal, replayEvents) || replayEvents.empty()) + { + outError = "Path manipulation smoke failed to build replay virtual events for the moved path goal."; + return false; + } + + bool hasRollReplay = false; + for (const auto& event : replayEvents) + { + if (event.type == CVirtualGimbalEvent::RollLeft || event.type == CVirtualGimbalEvent::RollRight) + { + hasRollReplay = true; + break; + } + } + if (!hasRollReplay) + { + outError = "Path manipulation smoke dropped the roll replay event for the moved path goal."; + return false; + } + + if (!state.pathCamera->manipulate({ replayEvents.data(), replayEvents.size() })) + { + outError = "Path manipulation smoke failed to replay path virtual events onto the baseline camera."; + return false; + } + + const auto replayCapture = state.goalSolver.captureDetailed(state.pathCamera); + if (!replayCapture.canUseGoal() || + !nbl::core::CCameraGoalUtilities::compareGoals( + replayCapture.goal, + movedCapture.goal, + nbl::system::SCameraSmokeComparisonThresholds::StrictPositionTolerance, + nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance)) + { + outError = "Path manipulation smoke failed the goal -> events -> manipulate replay roundtrip."; + return false; + } + + if (!restorePresetStrict( + state.goalSolver, + state.pathCamera, + state.initialPresets.path.value(), + "Path manipulation smoke failed to restore the baseline preset after replay", + outError)) + { + return false; + } + + if (!state.initialPresets.path->goal.hasTargetPosition) + { + outError = "Path manipulation smoke is missing the baseline path target state for custom path-limit validation."; + return false; + } + + const auto defaultPathModel = nbl::core::CCameraPathUtilities::makeDefaultPathModel(); + nbl::core::CPathCamera::path_model_t incompletePathModel = {}; + incompletePathModel.resolveState = defaultPathModel.resolveState; + + ICamera::PathStateLimits customPathLimits = { + .minU = 2.0, + .minDistance = 2.0, + .maxDistance = 3.0 + }; + auto customPathCamera = nbl::core::make_smart_refctd_ptr( + state.initialPresets.path->goal.position, + state.initialPresets.path->goal.targetPosition, + std::move(incompletePathModel), + customPathLimits); + + const auto& customPathModel = customPathCamera->getPathModel(); + if (!customPathModel.resolveState || !customPathModel.controlLaw || !customPathModel.integrate || !customPathModel.evaluate || !customPathModel.updateDistance) + { + outError = "Path manipulation smoke left a partially initialized path model active after constructor fallback."; + return false; + } + + ICamera::PathStateLimits resolvedPathLimits = {}; + if (!customPathCamera->tryGetPathStateLimits(resolvedPathLimits) || + hlsl::abs(resolvedPathLimits.minU - customPathLimits.minU) > CameraTinyScalarEpsilon || + hlsl::abs(resolvedPathLimits.minDistance - customPathLimits.minDistance) > CameraTinyScalarEpsilon || + hlsl::abs(resolvedPathLimits.maxDistance - customPathLimits.maxDistance) > CameraTinyScalarEpsilon) + { + outError = "Path manipulation smoke failed to expose custom per-camera path limits."; + return false; + } + + ICamera::SphericalTargetState customSphericalState = {}; + if (!customPathCamera->tryGetSphericalTargetState(customSphericalState) || + hlsl::abs(static_cast(customSphericalState.minDistance) - resolvedPathLimits.minDistance) > CameraTinyScalarEpsilon || + hlsl::abs(static_cast(customSphericalState.maxDistance) - resolvedPathLimits.maxDistance) > CameraTinyScalarEpsilon) + { + outError = "Path manipulation smoke failed to surface path limits through spherical target state."; + return false; + } + + ICamera::PathState customBaselinePathState = {}; + if (!customPathCamera->tryGetPathState(customBaselinePathState)) + { + outError = "Path manipulation smoke failed to capture the custom path-camera baseline state."; + return false; + } + + const double customBaselineDistance = hlsl::CCameraMathUtilities::getPathDistance(customBaselinePathState.u, customBaselinePathState.v); + if (customBaselineDistance + CameraTinyScalarEpsilon < resolvedPathLimits.minDistance || + customBaselineDistance - CameraTinyScalarEpsilon > resolvedPathLimits.maxDistance) + { + outError = "Path manipulation smoke failed to clamp the constructor-resolved path state to custom limits."; + return false; + } + + if (!customPathCamera->manipulate({ directPathEvents.data(), directPathEvents.size() })) + { + outError = "Path manipulation smoke failed to apply direct virtual events on the custom-limits path camera."; + return false; + } + + ICamera::PathState customManipulatedPathState = {}; + if (!customPathCamera->tryGetPathState(customManipulatedPathState)) + { + outError = "Path manipulation smoke failed to read the manipulated custom-limits path state."; + return false; + } + + ICamera::PathState expectedCustomPathState = {}; + if (!nbl::core::CCameraPathUtilities::tryApplyPathStateDelta( + customBaselinePathState, + nbl::core::CCameraPathUtilities::makePathDeltaFromVirtualPathMotion( + customPathCamera->scaleVirtualTranslation(directTranslationMagnitude), + customPathCamera->scaleVirtualRotation(hlsl::float64_t3(0.0, 0.0, directRollMagnitude))), + resolvedPathLimits, + expectedCustomPathState) || + !nbl::core::CCameraPathUtilities::pathStatesNearlyEqual( + customManipulatedPathState, + expectedCustomPathState, + nbl::core::SCameraPathDefaults::ExactComparisonThresholds)) + { + outError = "Path manipulation smoke failed the custom-limits default runtime mapping check."; + return false; + } + + const auto customMovedCapture = state.goalSolver.captureDetailed(customPathCamera.get()); + if (!customMovedCapture.canUseGoal()) + { + outError = "Path manipulation smoke failed to capture the moved custom-limits path goal."; + return false; + } + + if (!customPathCamera->trySetPathState(customBaselinePathState)) + { + outError = "Path manipulation smoke failed to restore the custom-limits baseline path state."; + return false; + } + + std::vector customReplayEvents; + if (!state.goalSolver.buildEvents(customPathCamera.get(), customMovedCapture.goal, customReplayEvents) || customReplayEvents.empty()) + { + outError = "Path manipulation smoke failed to build replay events for the custom-limits path goal."; + return false; + } + + if (!customPathCamera->manipulate({ customReplayEvents.data(), customReplayEvents.size() })) + { + outError = "Path manipulation smoke failed to replay events on the custom-limits path camera."; + return false; + } + + const auto customReplayCapture = state.goalSolver.captureDetailed(customPathCamera.get()); + if (!customReplayCapture.canUseGoal() || + !nbl::core::CCameraGoalUtilities::compareGoals( + customReplayCapture.goal, + customMovedCapture.goal, + nbl::system::SCameraSmokeComparisonThresholds::StrictPositionTolerance, + nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance)) + { + outError = "Path manipulation smoke failed the custom-limits goal replay roundtrip."; + return false; + } + } + + { + std::vector scaledEvents(3u); + scaledEvents[0].type = CVirtualGimbalEvent::MoveForward; + scaledEvents[0].magnitude = 2.0; + scaledEvents[1].type = CVirtualGimbalEvent::PanRight; + scaledEvents[1].magnitude = 3.0; + scaledEvents[2].type = CVirtualGimbalEvent::ScaleXInc; + scaledEvents[2].magnitude = 4.0; + nbl::core::CCameraManipulationUtilities::scaleVirtualEvents(scaledEvents, static_cast(scaledEvents.size()), 0.5f, 2.0f); + if (hlsl::abs(scaledEvents[0].magnitude - 1.0) > SCameraSmokeUtilityThresholds::VirtualEventScale || + hlsl::abs(scaledEvents[1].magnitude - 6.0) > SCameraSmokeUtilityThresholds::VirtualEventScale || + hlsl::abs(scaledEvents[2].magnitude - 4.0) > SCameraSmokeUtilityThresholds::VirtualEventScale) + { + outError = "Camera manipulation utilities smoke failed for virtual-event scaling."; + return false; + } + } + + { + const auto findEventMagnitude = [](const auto& events, const CVirtualGimbalEvent::VirtualEventType type) -> std::optional + { + for (const auto& event : events) + { + if (event.type == type) + return event.magnitude; + } + return std::nullopt; + }; + + const auto sumEventMagnitude = [](const auto& events, const CVirtualGimbalEvent::VirtualEventType type) -> double + { + double sum = 0.0; + for (const auto& event : events) + { + if (event.type == type) + sum += event.magnitude; + } + return sum; + }; + + const auto frameStepSeconds = std::chrono::duration(SCameraSmokeInputDefaults::EventStep).count(); + const auto expectedKeyboardMagnitude = + frameStepSeconds * nbl::ui::CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond; + + nbl::ui::CGimbalInputBinder inputBinder; + nbl::ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset( + inputBinder, + ICamera::CameraKind::FPS, + CVirtualGimbalEvent::All); + + const auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, nbl::ui::E_KEY_CODE::EKC_W); + const auto keyboardMagnitude = findEventMagnitude(keyboardEvents, CVirtualGimbalEvent::MoveForward); + if (!keyboardMagnitude.has_value() || + hlsl::abs(*keyboardMagnitude - expectedKeyboardMagnitude) > SCameraSmokeUtilityThresholds::VirtualEventScale) + { + outError = "Input binding smoke produced the wrong held-key magnitude for default FPS WASD."; + return false; + } + + inputBinder.clearBindingLayout(); + nbl::ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset( + inputBinder, + ICamera::CameraKind::FPS, + CVirtualGimbalEvent::All); + + const auto moveEvent = buildMovementSmokeMouseEvent(); + const std::array moveEvents = { moveEvent }; + const auto mouseEvents = collectMouseVirtualEvents(inputBinder, { moveEvents.data(), moveEvents.size() }); + const auto panMagnitude = findEventMagnitude(mouseEvents, CVirtualGimbalEvent::PanRight); + const auto tiltMagnitude = findEventMagnitude(mouseEvents, CVirtualGimbalEvent::TiltDown); + if (!panMagnitude.has_value() || + !tiltMagnitude.has_value() || + hlsl::abs(*panMagnitude - static_cast(SCameraSmokeInputDefaults::RelativeMouseMove)) > SCameraSmokeUtilityThresholds::VirtualEventScale || + hlsl::abs(*tiltMagnitude - hlsl::abs(static_cast(SCameraSmokeInputDefaults::RelativeMouseMoveY))) > SCameraSmokeUtilityThresholds::VirtualEventScale) + { + outError = "Input binding smoke produced the wrong relative-mouse magnitudes for default FPS look."; + return false; + } + + inputBinder.clearBindingLayout(); + nbl::ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset( + inputBinder, + ICamera::CameraKind::Orbit, + CVirtualGimbalEvent::All); + + const auto scrollEvent = buildScrollSmokeMouseEvent(); + const std::array scrollEvents = { scrollEvent }; + const auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { scrollEvents.data(), scrollEvents.size() }); + const auto scrollForwardMagnitude = sumEventMagnitude(mouseScrollEvents, CVirtualGimbalEvent::MoveForward); + if (hlsl::abs(scrollForwardMagnitude - static_cast(SCameraSmokeInputDefaults::VerticalScroll + SCameraSmokeInputDefaults::HorizontalScroll)) > SCameraSmokeUtilityThresholds::VirtualEventScale) + { + outError = "Input binding smoke produced the wrong scroll magnitude for default orbit zoom."; + return false; + } + + nbl::ui::CGimbalBindingLayoutStorage customLayout; + customLayout.updateKeyboardMapping([&](auto& map) + { + map[nbl::ui::E_KEY_CODE::EKC_W] = nbl::ui::IGimbalBindingLayout::CHashInfo(CVirtualGimbalEvent::MoveForward, 7.5); + }); + inputBinder.copyBindingLayoutFrom(customLayout); + + const auto customKeyboardEvents = collectKeyboardVirtualEvents(inputBinder, nbl::ui::E_KEY_CODE::EKC_W); + const auto customKeyboardMagnitude = findEventMagnitude(customKeyboardEvents, CVirtualGimbalEvent::MoveForward); + const auto expectedCustomKeyboardMagnitude = frameStepSeconds * 7.5; + if (!customKeyboardMagnitude.has_value() || + hlsl::abs(*customKeyboardMagnitude - expectedCustomKeyboardMagnitude) > SCameraSmokeUtilityThresholds::VirtualEventScale) + { + outError = "Input binding smoke failed to preserve binding-scale metadata through layout copies."; + return false; + } + } + + if (state.fpsCamera) + { + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.fpsCamera, "fps-motion-scale-baseline"); + if (!restorePresetStrict(state.goalSolver, state.fpsCamera, baselinePreset, "FPS motion-scale smoke failed to restore baseline before test", outError)) + return false; + + nbl::ui::CGimbalInputBinder inputBinder; + nbl::ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset( + inputBinder, + ICamera::CameraKind::FPS, + CVirtualGimbalEvent::All); + + const auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, nbl::ui::E_KEY_CODE::EKC_W); + const auto keyboardMagnitude = std::find_if( + keyboardEvents.begin(), + keyboardEvents.end(), + [](const CVirtualGimbalEvent& event) { return event.type == CVirtualGimbalEvent::MoveForward; }); + if (keyboardMagnitude == keyboardEvents.end()) + { + outError = "FPS motion-scale smoke failed to collect MoveForward event."; + return false; + } + + const auto baselinePosition = state.fpsCamera->getGimbal().getPosition(); + const auto baselineForward = state.fpsCamera->getGimbal().getZAxis(); + const auto expectedPositionDelta = + baselineForward * state.fpsCamera->scaleVirtualTranslation(static_cast(keyboardMagnitude->magnitude)); + if (!state.fpsCamera->manipulate({ keyboardEvents.data(), keyboardEvents.size() })) + { + outError = "FPS motion-scale smoke failed to apply collected keyboard events."; + return false; + } + + const auto actualPositionDelta = state.fpsCamera->getGimbal().getPosition() - baselinePosition; + if (!hlsl::CCameraMathUtilities::nearlyEqualVec3(actualPositionDelta, expectedPositionDelta, SCameraSmokeUtilityThresholds::PositionWriteback)) + { + outError = "FPS motion-scale smoke ignored camera-local translation scaling."; + return false; + } + + if (!restorePresetStrict(state.goalSolver, state.fpsCamera, baselinePreset, "FPS motion-scale smoke failed to restore baseline after test", outError)) + return false; + } + + if (state.freeCamera) + { + const auto freeBaselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.freeCamera, "free-motion-scale-baseline"); + if (!restorePresetStrict(state.goalSolver, state.freeCamera, freeBaselinePreset, "Free motion-scale smoke failed to restore baseline before test", outError)) + return false; + + { + std::array translationEvents = {{ + { CVirtualGimbalEvent::MoveForward, 2.0 } + }}; + const auto baselinePosition = state.freeCamera->getGimbal().getPosition(); + const auto baselineForward = state.freeCamera->getGimbal().getZAxis(); + const auto expectedPositionDelta = + baselineForward * state.freeCamera->scaleVirtualTranslation(translationEvents[0].magnitude); + if (!state.freeCamera->manipulate({ translationEvents.data(), translationEvents.size() })) + { + outError = "Free motion-scale smoke failed to apply translation event."; + return false; + } + + const auto actualPositionDelta = state.freeCamera->getGimbal().getPosition() - baselinePosition; + if (!hlsl::CCameraMathUtilities::nearlyEqualVec3(actualPositionDelta, expectedPositionDelta, SCameraSmokeUtilityThresholds::PositionWriteback)) + { + outError = "Free motion-scale smoke ignored camera-local translation scaling."; + return false; + } + + if (!restorePresetStrict(state.goalSolver, state.freeCamera, freeBaselinePreset, "Free motion-scale smoke failed to restore baseline after translation test", outError)) + return false; + } + + { + std::array rotationEvents = {{ + { CVirtualGimbalEvent::PanRight, 2.0 } + }}; + const auto baselineForward = state.freeCamera->getGimbal().getZAxis(); + const auto baselineUp = state.freeCamera->getGimbal().getYAxis(); + const auto expectedForward = hlsl::CCameraMathUtilities::rotateVectorByQuaternion( + hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle( + hlsl::normalize(baselineUp), + state.freeCamera->scaleVirtualRotation(rotationEvents[0].magnitude)), + baselineForward); + if (!state.freeCamera->manipulate({ rotationEvents.data(), rotationEvents.size() })) + { + outError = "Free motion-scale smoke failed to apply rotation event."; + return false; + } + + const auto actualForward = state.freeCamera->getGimbal().getZAxis(); + if (!hlsl::CCameraMathUtilities::nearlyEqualVec3(actualForward, expectedForward, SCameraSmokeUtilityThresholds::PositionWriteback)) + { + outError = "Free motion-scale smoke ignored camera-local rotation scaling."; + return false; + } + + if (!restorePresetStrict(state.goalSolver, state.freeCamera, freeBaselinePreset, "Free motion-scale smoke failed to restore baseline after rotation test", outError)) + return false; + } + + CameraPreset orientedPreset = state.initialPresets.free.value(); + orientedPreset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreeOrientationYawDeg); + const auto orientResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(state.goalSolver, state.freeCamera, orientedPreset); + if (!orientResult.succeeded() || !nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(state.goalSolver, state.freeCamera, orientedPreset)) + { + outError = "Camera manipulation utilities smoke failed to orient Free camera before translation remap."; + return false; + } + + std::vector worldTranslationEvents(3u); + worldTranslationEvents[0].type = CVirtualGimbalEvent::MoveRight; + worldTranslationEvents[0].magnitude = SCameraSmokeManipulationDefaults::WorldTranslationDelta.x; + worldTranslationEvents[1].type = CVirtualGimbalEvent::MoveUp; + worldTranslationEvents[1].magnitude = SCameraSmokeManipulationDefaults::WorldTranslationDelta.y; + worldTranslationEvents[2].type = CVirtualGimbalEvent::MoveForward; + worldTranslationEvents[2].magnitude = SCameraSmokeManipulationDefaults::WorldTranslationDelta.z; + uint32_t remappedCount = static_cast(worldTranslationEvents.size()); + nbl::core::CCameraManipulationUtilities::remapTranslationEventsFromWorldToCameraLocal(state.freeCamera, worldTranslationEvents, remappedCount); + if (remappedCount == 0u) + { + outError = "Camera manipulation utilities smoke produced empty translation remap."; + return false; + } + + if (!state.freeCamera->manipulate({ worldTranslationEvents.data(), remappedCount })) + { + outError = "Camera manipulation utilities smoke failed to apply remapped translation."; + return false; + } + + const auto remappedPosition = state.freeCamera->getGimbal().getPosition(); + const auto positionDelta = remappedPosition - orientedPreset.goal.position; + if (!hlsl::CCameraMathUtilities::nearlyEqualVec3(positionDelta, SCameraSmokeManipulationDefaults::WorldTranslationDelta, SCameraSmokeUtilityThresholds::PositionWriteback)) + { + outError = "Camera manipulation utilities smoke changed world-space translation semantics."; + return false; + } + + CameraPreset pitchPreset = state.initialPresets.free.value(); + pitchPreset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreePitchClampSourceDeg); + const auto pitchResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(state.goalSolver, state.freeCamera, pitchPreset); + if (!pitchResult.succeeded()) + { + outError = "Camera manipulation utilities smoke failed to prepare Free camera pitch clamp."; + return false; + } + + SCameraConstraintSettings freeConstraints = { + .enabled = true, + .clampPitch = true, + .pitchMinDeg = SCameraSmokeManipulationDefaults::PitchMinDeg, + .pitchMaxDeg = SCameraSmokeManipulationDefaults::PitchMaxDeg + }; + if (!nbl::this_example::CCameraConstraintUtilities::applyCameraConstraints(state.goalSolver, state.freeCamera, freeConstraints)) + { + outError = "Camera manipulation utilities smoke failed to clamp Free camera orientation."; + return false; + } + + const auto freeEulerDeg = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(state.freeCamera->getGimbal().getOrientation()); + if (hlsl::abs(static_cast(freeEulerDeg.x - SCameraSmokeManipulationDefaults::PitchMaxDeg)) > SCameraSmokeManipulationDefaults::PitchAppliedToleranceDeg) + { + outError = "Camera manipulation utilities smoke produced wrong clamped Free camera pitch."; + return false; + } + + const auto restoreFree = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(state.goalSolver, state.freeCamera, state.initialPresets.free.value()); + if (!restoreFree.succeeded() || !nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(state.goalSolver, state.freeCamera, state.initialPresets.free.value())) + { + outError = "Camera manipulation utilities smoke failed to restore Free camera baseline."; + return false; + } + } + + if (!verifyReferenceFrameSupportSmoke(state, outError)) + return false; + + if (state.initialPresets.orbit.has_value() && state.orbitCamera && state.initialPresets.orbit->goal.hasDistance) + { + CameraPreset farOrbitPreset = state.initialPresets.orbit.value(); + farOrbitPreset.goal.distance = state.initialPresets.orbit->goal.distance + SCameraSmokeManipulationDefaults::OrbitDistanceDelta; + const auto farOrbitResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(state.goalSolver, state.orbitCamera, farOrbitPreset); + if (!farOrbitResult.succeeded()) + { + outError = "Camera manipulation utilities smoke failed to prepare Orbit distance clamp."; + return false; + } + + SCameraConstraintSettings orbitConstraints = { + .enabled = true, + .clampDistance = true, + .minDistance = std::max( + SCameraSmokeManipulationDefaults::MinDistanceClampFloor, + state.initialPresets.orbit->goal.distance * SCameraSmokeManipulationDefaults::OrbitClampMinScale), + .maxDistance = state.initialPresets.orbit->goal.distance * SCameraSmokeManipulationDefaults::OrbitClampMaxScale + }; + if (!nbl::this_example::CCameraConstraintUtilities::applyCameraConstraints(state.goalSolver, state.orbitCamera, orbitConstraints)) + { + outError = "Camera manipulation utilities smoke failed to clamp Orbit distance."; + return false; + } + + ICamera::SphericalTargetState clampedOrbitState; + if (!state.orbitCamera->tryGetSphericalTargetState(clampedOrbitState) || + hlsl::abs(static_cast(clampedOrbitState.distance - orbitConstraints.maxDistance)) > SCameraSmokeUtilityThresholds::DynamicPerspectiveDelta) + { + outError = "Camera manipulation utilities smoke produced wrong clamped Orbit distance."; + return false; + } + + const auto restoreOrbit = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value()); + if (!restoreOrbit.succeeded() || !nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value())) + { + outError = "Camera manipulation utilities smoke failed to restore Orbit baseline."; + return false; + } + } + + if (state.initialPresets.dollyZoom.has_value() && state.dollyZoomCamera) + { + float dynamicFov = 0.0f; + if (!state.dollyZoomCamera->tryGetDynamicPerspectiveFov(dynamicFov)) + { + outError = "Camera projection utilities smoke failed to query DollyZoom dynamic FOV."; + return false; + } + + auto perspectiveProjection = IPlanarProjection::CProjection::create( + SCameraSmokeManipulationDefaults::PerspectiveNearPlane, + SCameraSmokeManipulationDefaults::PerspectiveFarPlane, + SCameraSmokeManipulationDefaults::PerspectiveFovDeg); + if (!nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(state.dollyZoomCamera, perspectiveProjection)) + { + outError = "Camera projection utilities smoke failed to sync dynamic perspective projection."; + return false; + } + if (hlsl::abs(static_cast(perspectiveProjection.getParameters().m_planar.perspective.fov - dynamicFov)) > SCameraSmokeUtilityThresholds::DynamicPerspectiveDelta) + { + outError = "Camera projection utilities smoke produced wrong dynamic perspective FOV."; + return false; + } + + auto orthographicProjection = IPlanarProjection::CProjection::create( + SCameraSmokeManipulationDefaults::PerspectiveNearPlane, + SCameraSmokeManipulationDefaults::PerspectiveFarPlane, + SCameraSmokeManipulationDefaults::OrthoExtent); + if (nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(state.dollyZoomCamera, orthographicProjection)) + { + outError = "Camera projection utilities smoke unexpectedly synced orthographic projection."; + return false; + } + } + + if (CCameraTextUtilities::getCameraTypeLabel(ICamera::CameraKind::DollyZoom) != "Dolly Zoom") + { + outError = "Camera text utilities smoke failed for Dolly Zoom label."; + return false; + } + if (CCameraTextUtilities::getCameraTypeDescription(ICamera::CameraKind::Path) != std::string(nbl::core::SCameraPathRigMetadata::KindDescription)) + { + outError = "Camera text utilities smoke failed for Path description."; + return false; + } + if (CCameraTextUtilities::describeGoalStateMask(ICamera::GoalStateNone) != "Pose only") + { + outError = "Camera text utilities smoke failed for empty goal-state description."; + return false; + } + const ICamera::goal_state_flags_t combinedGoalStateMask = ICamera::goal_state_flags_t(ICamera::GoalStateSphericalTarget) | ICamera::goal_state_flags_t(ICamera::GoalStateDynamicPerspective); + if (CCameraTextUtilities::describeGoalStateMask(combinedGoalStateMask) != "Spherical target, Dynamic perspective") + { + outError = "Camera text utilities smoke failed for combined goal-state description."; + return false; + } + + CCameraGoalSolver::SApplyResult defaultApplyResult; + const auto applyResultText = CCameraTextUtilities::describeApplyResult(defaultApplyResult); + if (applyResultText.find("status=Unsupported") == std::string::npos || applyResultText.find("events=0") == std::string::npos) + { + outError = "Camera text utilities smoke failed for apply-result description."; + return false; + } + + SCameraPresetApplySummary summary; + summary.targetCount = 2u; + summary.successCount = 2u; + summary.approximateCount = 1u; + const auto summaryText = nbl::ui::CCameraTextUtilities::describePresetApplySummary(summary, "none"); + if (summaryText.find("targets=2") == std::string::npos || summaryText.find("approximate=1") == std::string::npos) + { + outError = "Camera text utilities smoke failed for preset-apply summary description."; + return false; + } + + return true; + } + + template + inline bool verifyFollowSmoke( + const SCameraSmokeResolvedState& state, + std::span> cameras, + std::span> planarSpan, + TMakeDefaultFollowConfig&& makeDefaultFollowConfig, + TVerifyMarkerAlignment&& verifyMarkerAlignment, + TVerifyOffsetRecapture&& verifyOffsetRecapture, + std::string& outError) + { + CTrackedTarget trackedTarget( + SCameraSmokeFollowScenario::InitialTargetPosition, + SCameraSmokeFollowScenario::InitialTargetOrientation, + "Smoke Target"); + + const auto& movedTrackedTargetPosition = SCameraSmokeFollowScenario::MovedTargetPosition; + const auto& movedTrackedTargetOrientation = SCameraSmokeFollowScenario::MovedTargetOrientation; + + if (state.orbitCamera) + { + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.orbitCamera, "orbit-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::OrbitTarget; + + if (!validateFollowScenario(state.goalSolver, planarSpan, state.orbitCamera, trackedTarget, followConfig, "orbit follow", outError)) + return false; + if (!verifyMarkerAlignment(trackedTarget, "orbit follow", outError)) + return false; + + if (!restorePresetStrict(state.goalSolver, state.orbitCamera, baselinePreset, "Orbit follow smoke failed to restore the baseline preset", outError)) + return false; + + followConfig.mode = ECameraFollowMode::KeepWorldOffset; + followConfig.worldOffset = SCameraSmokeFollowScenario::OrbitWorldOffset; + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + if (!validateFollowScenario(state.goalSolver, planarSpan, state.orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow", outError)) + return false; + if (!restorePresetStrict(state.goalSolver, state.orbitCamera, baselinePreset, "Orbit keep-world-offset smoke failed to restore the baseline preset", outError)) + return false; + } + + for (const auto& cameraRef : cameras) + { + auto* defaultFollowCamera = cameraRef.get(); + if (!defaultFollowCamera) + continue; + + auto followConfig = makeDefaultFollowConfig(defaultFollowCamera); + if (!followConfig.enabled || followConfig.mode == ECameraFollowMode::Disabled) + continue; + + const auto label = std::string(defaultFollowCamera->getIdentifier()) + " default follow"; + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, defaultFollowCamera, label + " baseline"); + + trackedTarget.setPose( + SCameraSmokeFollowScenario::InitialTargetPosition, + SCameraSmokeFollowScenario::InitialTargetOrientation); + if ((nbl::core::CCameraFollowUtilities::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::core::CCameraFollowUtilities::cameraFollowModeUsesWorldOffset(followConfig.mode)) && + !nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(state.goalSolver, defaultFollowCamera, trackedTarget, followConfig)) + { + outError = "Default follow smoke failed to capture offsets for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."; + return false; + } + + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + if (!validateFollowScenario(state.goalSolver, planarSpan, defaultFollowCamera, trackedTarget, followConfig, label, outError)) + return false; + if (!verifyMarkerAlignment(trackedTarget, label, outError)) + return false; + + if (!restorePresetStrict( + state.goalSolver, + defaultFollowCamera, + baselinePreset, + "Default follow smoke failed to restore the baseline preset for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"", + outError)) + { + return false; + } + } + + if (state.freeCamera) + { + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.freeCamera, "free-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::LookAtTarget; + + if (!validateFollowScenario(state.goalSolver, planarSpan, state.freeCamera, trackedTarget, followConfig, "free look-at follow", outError)) + return false; + if (!verifyMarkerAlignment(trackedTarget, "free look-at follow", outError)) + return false; + + if (!restorePresetStrict(state.goalSolver, state.freeCamera, baselinePreset, "Free follow smoke failed to restore the baseline preset", outError)) + return false; + + followConfig.mode = ECameraFollowMode::KeepWorldOffset; + followConfig.worldOffset = SCameraSmokeFollowScenario::FreeWorldOffset; + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + if (!validateFollowScenario(state.goalSolver, planarSpan, state.freeCamera, trackedTarget, followConfig, "free keep-world-offset follow", outError)) + return false; + if (!restorePresetStrict(state.goalSolver, state.freeCamera, baselinePreset, "Free keep-world-offset smoke failed to restore the baseline preset", outError)) + return false; + } + + if (state.chaseCamera) + { + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.chaseCamera, "chase-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::KeepLocalOffset; + if (!nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(state.goalSolver, state.chaseCamera, trackedTarget, followConfig)) + { + outError = "Chase follow smoke failed to capture local offset."; + return false; + } + + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + if (!validateFollowScenario(state.goalSolver, planarSpan, state.chaseCamera, trackedTarget, followConfig, "chase local-offset follow", outError)) + return false; + if (!verifyMarkerAlignment(trackedTarget, "chase local-offset follow", outError)) + return false; + + if (!restorePresetStrict(state.goalSolver, state.chaseCamera, baselinePreset, "Chase follow smoke failed to restore the baseline preset", outError)) + return false; + } + + if (!verifyOffsetRecapture(state.chaseCamera, trackedTarget, "chase follow recapture", outError)) + return false; + if (!verifyOffsetRecapture(state.dollyCamera, trackedTarget, "dolly follow recapture", outError)) + return false; + + return true; + } + diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl new file mode 100644 index 000000000..dc3393aa8 --- /dev/null +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -0,0 +1,1464 @@ + using camera_json_t = nlohmann::json; + using CameraPreset = nbl::core::CCameraPreset; + constexpr double CameraTinyScalarEpsilon = nbl::system::SCameraSmokeComparisonThresholds::TinyScalarEpsilon; + constexpr nbl::system::SCameraFollowRegressionThresholds CameraFollowRegressionThresholds = {}; + + struct SCameraSmokePersistenceThresholds final + { + static constexpr double PositionTolerance = nbl::system::SCameraSmokeComparisonThresholds::StrictPositionTolerance; + static constexpr double AngularToleranceDeg = nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg; + static constexpr double ScalarTolerance = nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance; + }; + + struct SCameraSmokeInputDefaults final + { + static constexpr auto EventStep = std::chrono::microseconds(16667); + static constexpr int32_t RelativeMouseMove = 12; + static constexpr int32_t RelativeMouseMoveY = -8; + static constexpr int32_t VerticalScroll = 4; + static constexpr int32_t HorizontalScroll = 2; + }; + + struct SCameraSmokeScriptedCheckDefaults final + { + static inline const float64_t3 OrbitCameraPosition = float64_t3(0.0, 1.5, -6.0); + static inline const float64_t3 OrbitCameraTarget = float64_t3(0.0, 0.0, 0.0); + static inline const float64_t3 InitialTrackedTargetPosition = float64_t3(2.0, 0.5, -1.5); + static inline const camera_quaternion_t InitialTrackedTargetOrientation = + hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(float64_t3(0.0, 1.0, 0.0), hlsl::radians(35.0)); + static constexpr uint64_t BaselineFrame = 1u; + static constexpr uint64_t StepFrame = 2u; + static constexpr uint64_t FollowLockFrame = 3u; + static constexpr float PositionTolerance = 2.0f; + static constexpr float MinPositionDelta = 0.005f; + static constexpr float AngularToleranceDeg = 45.0f; + static constexpr float MinAngularDeltaDeg = 0.05f; + static constexpr double StepEventMagnitude = 12.0; + }; + + struct SCameraSmokeFollowScenario final + { + static inline const float64_t3 InitialTargetPosition = float64_t3(2.25, -0.75, 1.25); + static inline const camera_quaternion_t InitialTargetOrientation = + hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadians(float64_t3(0.18, -0.22, 0.41)); + static inline const float64_t3 MovedTargetPosition = float64_t3(-1.5, 0.5, 2.25); + static inline const camera_quaternion_t MovedTargetOrientation = + hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadians(float64_t3(-0.12, 0.35, 0.27)); + static inline const float64_t3 OrbitWorldOffset = float64_t3(4.0, -1.5, 2.0); + static inline const float64_t3 FreeWorldOffset = float64_t3(5.0, -2.0, 1.5); + static constexpr double OrbitRecaptureDeltaDeg = 18.0; + static constexpr float OrbitRecaptureDistanceDelta = 0.75f; + }; + + struct SCameraSmokeManipulationDefaults final + { + static inline const float64_t3 WorldTranslationDelta = float64_t3(1.25, 0.5, 2.0); + static inline const float64_t3 FreeOrientationYawDeg = float64_t3(0.0, 90.0, 0.0); + static inline const float64_t3 FreePitchClampSourceDeg = float64_t3(60.0, 0.0, 0.0); + static constexpr float PitchMinDeg = -15.0f; + static constexpr float PitchMaxDeg = 15.0f; + static constexpr double PitchAppliedToleranceDeg = 0.1; + static constexpr float MinDistanceClampFloor = 0.1f; + static constexpr float OrbitClampMinScale = 0.5f; + static constexpr float OrbitClampMaxScale = 0.75f; + static constexpr float OrbitDistanceDelta = 10.0f; + static constexpr float PerspectiveNearPlane = 0.1f; + static constexpr float PerspectiveFarPlane = 100.0f; + static constexpr float PerspectiveFovDeg = 60.0f; + static constexpr float OrthoExtent = 10.0f; + }; + + struct SCameraSmokeDynamicPerspectiveDefaults final + { + static constexpr float BaseFovDeltaDeg = 7.5f; + static constexpr float BaseFovMinDeg = 10.0f; + static constexpr float BaseFovMaxDeg = 150.0f; + static constexpr float ReferenceDistanceDelta = 1.25f; + static constexpr float ReferenceDistanceMin = 0.1f; + }; + + struct SCameraSmokeUtilityThresholds final + { + static constexpr double PositionWriteback = nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance; + static constexpr double DynamicPerspectiveDelta = nbl::system::SCameraSmokeComparisonThresholds::StrictScalarTolerance; + static constexpr double VirtualEventScale = CameraTinyScalarEpsilon; + }; + + struct SCameraSmokePresetMutationDefaults final + { + static inline const float64_t3 TargetOffset = float64_t3(0.5, -0.25, 0.75); + static constexpr double DirectEventMagnitude = 1.0; + }; + + struct SCameraSmokeSequenceDefaults final + { + static constexpr float Fps = 4.0f; + static constexpr float DurationSeconds = 2.0f; + static constexpr std::array CaptureFractions = { 0.0f, 0.5f, 1.0f }; + static constexpr float SecondKeyframeTime = 1.0f; + static constexpr uint64_t DurationFrames = 8ull; + static constexpr std::array CaptureFrameOffsets = { 0ull, 4ull, 7ull }; + static constexpr uint32_t AvailableWindowCount = 2u; + static constexpr uint32_t PlanarIx = 5u; + static constexpr uint64_t StartFrame = 11u; + static constexpr std::array CaptureFrames = { 11ull, 15ull, 18ull }; + static constexpr size_t BaselineCheckCount = 1u; + static constexpr size_t ContinuityCheckCount = 7u; + static constexpr size_t FollowCheckCount = 7u; + static inline const float64_t3 TargetPositionA = float64_t3(1.0, 2.0, 3.0); + static inline const float64_t3 TargetPositionB = float64_t3(4.0, 5.0, 6.0); + static inline const float64_t3 TargetPositionC = float64_t3(7.0, 8.0, 9.0); + }; + + struct SCameraSmokePlaybackDefaults final + { + static constexpr float EndKeyframeTime = 2.0f; + static constexpr float MidPlaybackTime = 1.5f; + static constexpr float ResetPlaybackTime = 1.25f; + static constexpr float OvershootPlaybackTime = 9.0f; + static constexpr float AdvanceDt = 1.0f; + static constexpr float WrappedPlaybackTime = 0.5f; + }; + + struct SCameraSmokeRuntimeDefaults final + { + static constexpr uint64_t ActionFrame = 3u; + static constexpr uint64_t FollowFrame = 4u; + static constexpr int32_t ActivePlanarValue = 4; + static inline constexpr std::string_view SegmentLabel = "segment-three"; + static inline const float64_t3 GoalPosition = float64_t3(1.0, 2.0, 3.0); + static inline const float64_t3 TrackedTargetPosition = float64_t3(7.0, 8.0, 9.0); + }; + + struct SCameraSmokeRuntimeParserDefaults final + { + static inline constexpr std::string_view CapturePrefix = "parser_smoke"; + static constexpr double KeyboardScale = 2.0; + static constexpr double RotationScale = 0.5; + static constexpr uint64_t EventFrame = 2u; + static constexpr uint64_t StepFrame = 3u; + static constexpr int32_t ActivePlanarValue = 3; + static constexpr float MinPositionDelta = 0.01f; + static constexpr float MaxPositionDelta = 1.0f; + }; + + struct SCameraSmokePresetInventory final + { + std::optional orbit = std::nullopt; + std::optional free = std::nullopt; + std::optional chase = std::nullopt; + std::optional dolly = std::nullopt; + std::optional path = std::nullopt; + std::optional dollyZoom = std::nullopt; + }; + + struct SCameraSmokeCameraInventory final + { + ICamera* fps = nullptr; + ICamera* orbit = nullptr; + ICamera* arcball = nullptr; + ICamera* turntable = nullptr; + ICamera* topDown = nullptr; + ICamera* isometric = nullptr; + ICamera* free = nullptr; + ICamera* chase = nullptr; + ICamera* dolly = nullptr; + ICamera* path = nullptr; + ICamera* dollyZoom = nullptr; + }; + + enum class EPresetComparePolicy : uint8_t + { + None, + DefaultThresholds, + StrictThresholds + }; + + inline bool reportHeadlessCameraSmokeFailure(App& app, const std::string& message) + { + std::cerr << "[headless-camera-smoke][fail] " << message << std::endl; + (void)app; + return false; + } + + inline std::vector collectKeyboardVirtualEvents( + CGimbalInputBinder& inputBinder, + const ui::E_KEY_CODE keyCode) + { + static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); + smokeTimestamp += SCameraSmokeInputDefaults::EventStep; + const auto pressTs = smokeTimestamp; + + SKeyboardEvent pressEvent(pressTs); + pressEvent.keyCode = keyCode; + pressEvent.action = SKeyboardEvent::ECA_PRESSED; + pressEvent.window = nullptr; + + inputBinder.collectVirtualEvents(pressTs, { .keyboardEvents = { &pressEvent, 1u } }); + + smokeTimestamp += SCameraSmokeInputDefaults::EventStep; + const auto sampleTs = smokeTimestamp; + return inputBinder.collectVirtualEvents(sampleTs).events; + } + + inline std::vector collectMouseVirtualEvents( + CGimbalInputBinder& inputBinder, + std::span mouseEvents) + { + static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); + smokeTimestamp += SCameraSmokeInputDefaults::EventStep; + return inputBinder.collectVirtualEvents(smokeTimestamp, { .mouseEvents = mouseEvents }).events; + } + + inline std::vector filterOrbitMouseEvents( + ICamera* const camera, + std::span input, + const bool orbitLookDown) + { + if (!(camera && camera->hasCapability(ICamera::SphericalTarget))) + return std::vector(input.begin(), input.end()); + + std::vector filtered; + filtered.reserve(input.size()); + for (const auto& event : input) + { + if (event.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) + continue; + filtered.emplace_back(event); + } + return filtered; + } + + inline SMouseEvent buildMovementSmokeMouseEvent() + { + SMouseEvent event(SCameraSmokeInputDefaults::EventStep); + event.window = nullptr; + event.type = ui::SMouseEvent::EET_MOVEMENT; + event.movementEvent.relativeMovementX = SCameraSmokeInputDefaults::RelativeMouseMove; + event.movementEvent.relativeMovementY = SCameraSmokeInputDefaults::RelativeMouseMoveY; + return event; + } + + inline SMouseEvent buildScrollSmokeMouseEvent() + { + SMouseEvent event(SCameraSmokeInputDefaults::EventStep); + event.window = nullptr; + event.type = ui::SMouseEvent::EET_SCROLL; + event.scrollEvent.verticalScroll = SCameraSmokeInputDefaults::VerticalScroll; + event.scrollEvent.horizontalScroll = SCameraSmokeInputDefaults::HorizontalScroll; + return event; + } + + inline void buildDirectManipulationEvents( + const uint32_t allowedEvents, + std::vector& outEvents) + { + outEvents.clear(); + outEvents.reserve(3u); + + const auto appendEvent = [&](const CVirtualGimbalEvent::VirtualEventType type) + { + outEvents.emplace_back(CVirtualGimbalEvent{ + .type = type, + .magnitude = SCameraSmokePresetMutationDefaults::DirectEventMagnitude + }); + }; + + const auto tryAppendFirstAllowedEvent = [&](const std::span candidates) -> bool + { + for (const auto event : candidates) + { + if ((allowedEvents & event) != event) + continue; + if (std::find_if(outEvents.begin(), outEvents.end(), [&](const CVirtualGimbalEvent& existing) { return existing.type == event; }) != outEvents.end()) + continue; + + appendEvent(event); + return true; + } + return false; + }; + + static constexpr std::array PreferredTranslationEvents = { + CVirtualGimbalEvent::MoveForward, + CVirtualGimbalEvent::MoveRight, + CVirtualGimbalEvent::MoveUp, + CVirtualGimbalEvent::MoveLeft, + CVirtualGimbalEvent::MoveDown, + CVirtualGimbalEvent::MoveBackward + }; + static constexpr std::array PreferredRotationEvents = { + CVirtualGimbalEvent::PanRight, + CVirtualGimbalEvent::TiltUp, + CVirtualGimbalEvent::RollRight + }; + + const bool appendedTranslation = tryAppendFirstAllowedEvent(PreferredTranslationEvents); + const bool appendedRotation = tryAppendFirstAllowedEvent(PreferredRotationEvents); + if (appendedTranslation && !appendedRotation) + tryAppendFirstAllowedEvent(PreferredTranslationEvents); + + if (!outEvents.empty()) + return; + + for (const auto event : CVirtualGimbalEvent::VirtualEventsTypeTable) + { + if ((allowedEvents & event) != event) + continue; + + appendEvent(event); + return; + } + } + + inline ICamera* findCameraByKind( + const std::span> cameras, + const ICamera::CameraKind kind) + { + for (const auto& cameraRef : cameras) + { + auto* const camera = cameraRef.get(); + if (camera && camera->getKind() == kind) + return camera; + } + return nullptr; + } + + inline uint32_t expectedMissingGoalStateMaskForIssue(const CCameraGoalSolver::SApplyResult::EIssue issue) + { + switch (issue) + { + case CCameraGoalSolver::SApplyResult::EIssue::MissingPathState: + return ICamera::GoalStatePath; + case CCameraGoalSolver::SApplyResult::EIssue::MissingDynamicPerspectiveState: + return ICamera::GoalStateDynamicPerspective; + case CCameraGoalSolver::SApplyResult::EIssue::MissingSphericalTargetState: + return ICamera::GoalStateSphericalTarget; + default: + return ICamera::GoalStateNone; + } + } + + inline void storeInitialPresetForKind( + const ICamera::CameraKind kind, + const CameraPreset& preset, + SCameraSmokePresetInventory& inventory) + { + switch (kind) + { + case ICamera::CameraKind::Orbit: + inventory.orbit = preset; + break; + case ICamera::CameraKind::Free: + inventory.free = preset; + break; + case ICamera::CameraKind::Chase: + inventory.chase = preset; + break; + case ICamera::CameraKind::Dolly: + inventory.dolly = preset; + break; + case ICamera::CameraKind::Path: + inventory.path = preset; + break; + case ICamera::CameraKind::DollyZoom: + inventory.dollyZoom = preset; + break; + default: + break; + } + } + + inline SCameraSmokeCameraInventory collectSmokeCameras(const std::span> cameras) + { + return { + .fps = findCameraByKind(cameras, ICamera::CameraKind::FPS), + .orbit = findCameraByKind(cameras, ICamera::CameraKind::Orbit), + .arcball = findCameraByKind(cameras, ICamera::CameraKind::Arcball), + .turntable = findCameraByKind(cameras, ICamera::CameraKind::Turntable), + .topDown = findCameraByKind(cameras, ICamera::CameraKind::TopDown), + .isometric = findCameraByKind(cameras, ICamera::CameraKind::Isometric), + .free = findCameraByKind(cameras, ICamera::CameraKind::Free), + .chase = findCameraByKind(cameras, ICamera::CameraKind::Chase), + .dolly = findCameraByKind(cameras, ICamera::CameraKind::Dolly), + .path = findCameraByKind(cameras, ICamera::CameraKind::Path), + .dollyZoom = findCameraByKind(cameras, ICamera::CameraKind::DollyZoom) + }; + } + + inline bool cameraMatchesPreset( + const CCameraGoalSolver& goalSolver, + ICamera* const camera, + const CameraPreset& preset, + const EPresetComparePolicy comparePolicy) + { + switch (comparePolicy) + { + case EPresetComparePolicy::None: + return true; + case EPresetComparePolicy::DefaultThresholds: + return nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithDefaultThresholds(goalSolver, camera, preset); + case EPresetComparePolicy::StrictThresholds: + return nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset); + default: + return false; + } + } + + inline std::string buildPresetSmokeMismatchMessage( + std::string_view prefix, + const CCameraGoalSolver& goalSolver, + ICamera* const camera, + const CameraPreset& preset) + { + return std::string(prefix) + " " + nbl::core::CCameraPresetFlowUtilities::describePresetCameraMismatch(goalSolver, camera, preset); + } + + inline bool applyPresetAndValidate( + const CCameraGoalSolver& goalSolver, + ICamera* const camera, + const CameraPreset& preset, + const EPresetComparePolicy comparePolicy, + const bool requireChanged, + const bool requireExact, + const std::string_view failurePrefix, + std::string& outError) + { + const auto applyResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(goalSolver, camera, preset); + if (!applyResult.succeeded() || + (requireChanged && !applyResult.changed()) || + (requireExact && !applyResult.exact)) + { + outError = std::string(failurePrefix) + ". " + CCameraTextUtilities::describeApplyResult(applyResult); + return false; + } + + if (!cameraMatchesPreset(goalSolver, camera, preset, comparePolicy)) + { + outError = buildPresetSmokeMismatchMessage(failurePrefix, goalSolver, camera, preset); + return false; + } + + return true; + } + + inline bool restorePresetStrict( + const CCameraGoalSolver& goalSolver, + ICamera* const camera, + const CameraPreset& preset, + const std::string_view failurePrefix, + std::string& outError) + { + const auto restoreResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(goalSolver, camera, preset); + if (restoreResult.succeeded() && nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset)) + return true; + + outError = std::string(failurePrefix) + ". " + CCameraTextUtilities::describeApplyResult(restoreResult); + if (camera) + outError += " " + nbl::core::CCameraPresetFlowUtilities::describePresetCameraMismatch(goalSolver, camera, preset); + return false; + } + + inline bool buildAndValidateFollowTargetContract( + const CCameraGoalSolver& solver, + std::span> planarProjections, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, + nbl::system::SCameraFollowApplyValidationResult& outResult, + std::string* const outError) + { + std::string regressionError; + if (nbl::system::CCameraFollowRegressionUtilities::buildApplyAndValidateFollowTargetContract( + solver, + camera, + trackedTarget, + followConfig, + outResult, + ®ressionError, + nullptr)) + { + nbl::system::SCameraProjectionContext projectionContext = {}; + if (!nbl::ui::tryBuildCameraProjectionContext(planarProjections, camera, projectionContext)) + return true; + + nbl::system::SCameraFollowRegressionResult postApplyRegression = {}; + if (!nbl::system::CCameraFollowRegressionUtilities::validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + outResult.goal, + postApplyRegression, + ®ressionError, + &projectionContext, + CameraFollowRegressionThresholds)) + { + if (outError) + *outError = regressionError; + return false; + } + + outResult.regression = postApplyRegression; + return true; + } + + if (outError) + *outError = regressionError; + return false; + } + + inline SCameraFollowVisualMetrics buildFollowVisualMetricsForCamera( + const std::span> planarProjections, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig); + + inline bool verifyFollowVisualMetrics( + const std::span> planarProjections, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, + const char* const label, + std::string* const outError) + { + const auto metrics = buildFollowVisualMetricsForCamera(planarProjections, camera, trackedTarget, followConfig); + nbl::system::SCameraProjectionContext projectionContext = {}; + const bool expectsProjectedMetrics = nbl::ui::tryBuildCameraProjectionContext(planarProjections, camera, projectionContext); + if (!metrics.active) + { + if (outError) + *outError = std::string("Follow visual metrics smoke was inactive for ") + label + "."; + return false; + } + if (nbl::core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) + { + if (outError) + *outError = std::string("Follow visual metrics smoke was missing lock metrics for ") + label + "."; + return false; + } + if (expectsProjectedMetrics && !metrics.projectedValid) + { + if (outError) + *outError = std::string("Follow visual metrics smoke was missing projected metrics for ") + label + "."; + return false; + } + if (metrics.projectedValid && metrics.projectedTarget.radius > CameraFollowRegressionThresholds.projectedNdcTolerance) + { + if (outError) + { + const auto targetPosition = trackedTarget.getGimbal().getPosition(); + const auto cameraPosition = camera ? camera->getGimbal().getPosition() : float64_t3(0.0); + const auto viewMatrix = camera ? hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(camera->getGimbal().getViewMatrix()) : float64_t4x4(1.0); + const auto targetView = hlsl::mul(viewMatrix, float64_t4(targetPosition, 1.0)); + std::ostringstream oss; + oss << "Follow visual metrics smoke had projected center error for " << label + << ". ndc=(" << metrics.projectedTarget.ndc.x << ", " << metrics.projectedTarget.ndc.y << ")" + << " radius=" << metrics.projectedTarget.radius + << " lock_deg=" << metrics.lockAngleDeg + << " target_distance=" << metrics.targetDistance + << " camera_pos=(" << cameraPosition.x << ", " << cameraPosition.y << ", " << cameraPosition.z << ")" + << " target_pos=(" << targetPosition.x << ", " << targetPosition.y << ", " << targetPosition.z << ")" + << " target_view=(" << targetView.x << ", " << targetView.y << ", " << targetView.z << ", " << targetView.w << ")"; + *outError = oss.str(); + } + return false; + } + return true; + } + + inline bool validateFollowScenario( + const CCameraGoalSolver& goalSolver, + std::span> planarSpan, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, + const std::string_view label, + std::string& outError) + { + nbl::system::SCameraFollowApplyValidationResult followResult = {}; + std::string followError; + if (!buildAndValidateFollowTargetContract( + goalSolver, + planarSpan, + camera, + trackedTarget, + followConfig, + followResult, + &followError)) + { + outError = std::string("Follow smoke validation failed for ") + std::string(label) + ". " + followError; + return false; + } + if (!verifyFollowVisualMetrics(planarSpan, camera, trackedTarget, followConfig, label.data(), &followError)) + { + outError = followError; + return false; + } + return true; + } + + inline bool runPerCameraPresetAndBindingSmoke( + const CCameraGoalSolver& goalSolver, + const std::span> cameras, + SCameraSmokePresetInventory& initialPresets, + std::string& outError) + { + for (const auto& cameraRef : cameras) + { + auto* const camera = cameraRef.get(); + if (!camera) + { + outError = "Null camera instance."; + return false; + } + + CGimbalInputBinder inputBinder; + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); + + const std::string cameraIdentifier(camera->getIdentifier()); + const auto initialPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, "smoke-initial"); + const auto initialCompatibility = nbl::core::CCameraGoalAnalysisUtilities::analyzePresetApply(goalSolver, camera, initialPreset).compatibility; + if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) + { + outError = "Preset compatibility smoke failed for camera \"" + cameraIdentifier + + "\". missing=" + CCameraTextUtilities::describeGoalStateMask(initialCompatibility.missingGoalStateMask); + return false; + } + + storeInitialPresetForKind(camera->getKind(), initialPreset, initialPresets); + + if (!nbl::core::CCameraPresetFlowUtilities::applyPreset(goalSolver, camera, initialPreset)) + { + outError = "Preset no-op smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + + if (initialPreset.goal.hasTargetPosition) + { + CameraPreset shiftedPreset = initialPreset; + shiftedPreset.goal.targetPosition += SCameraSmokePresetMutationDefaults::TargetOffset; + + if (!applyPresetAndValidate( + goalSolver, + camera, + shiftedPreset, + EPresetComparePolicy::None, + true, + true, + "Preset target apply smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + + ICamera::SphericalTargetState shiftedState; + if (!camera->tryGetSphericalTargetState(shiftedState) || + !hlsl::CCameraMathUtilities::nearlyEqualVec3(shiftedState.target, shiftedPreset.goal.targetPosition, SCameraSmokeUtilityThresholds::PositionWriteback)) + { + outError = "Preset target writeback smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + + if (!applyPresetAndValidate( + goalSolver, + camera, + initialPreset, + EPresetComparePolicy::DefaultThresholds, + false, + true, + "Preset restore smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + + ICamera::SphericalTargetState restoredState; + if (!camera->tryGetSphericalTargetState(restoredState) || + !hlsl::CCameraMathUtilities::nearlyEqualVec3(restoredState.target, initialPreset.goal.targetPosition, SCameraSmokeUtilityThresholds::PositionWriteback)) + { + outError = "Preset target restore smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + } + + if (initialPreset.goal.hasDynamicPerspectiveState) + { + CameraPreset shiftedPreset = initialPreset; + shiftedPreset.goal.dynamicPerspectiveState.baseFov = + std::clamp( + initialPreset.goal.dynamicPerspectiveState.baseFov + SCameraSmokeDynamicPerspectiveDefaults::BaseFovDeltaDeg, + SCameraSmokeDynamicPerspectiveDefaults::BaseFovMinDeg, + SCameraSmokeDynamicPerspectiveDefaults::BaseFovMaxDeg); + if (hlsl::abs(static_cast( + shiftedPreset.goal.dynamicPerspectiveState.baseFov - + initialPreset.goal.dynamicPerspectiveState.baseFov)) < SCameraSmokeUtilityThresholds::DynamicPerspectiveDelta) + { + shiftedPreset.goal.dynamicPerspectiveState.baseFov = + std::max( + SCameraSmokeDynamicPerspectiveDefaults::BaseFovMinDeg, + initialPreset.goal.dynamicPerspectiveState.baseFov - SCameraSmokeDynamicPerspectiveDefaults::BaseFovDeltaDeg); + } + shiftedPreset.goal.dynamicPerspectiveState.referenceDistance = + std::max( + SCameraSmokeDynamicPerspectiveDefaults::ReferenceDistanceMin, + initialPreset.goal.dynamicPerspectiveState.referenceDistance + SCameraSmokeDynamicPerspectiveDefaults::ReferenceDistanceDelta); + + if (!applyPresetAndValidate( + goalSolver, + camera, + shiftedPreset, + EPresetComparePolicy::StrictThresholds, + true, + false, + "Preset dynamic perspective apply smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + + if (!applyPresetAndValidate( + goalSolver, + camera, + initialPreset, + EPresetComparePolicy::StrictThresholds, + false, + false, + "Preset dynamic perspective restore smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + } + + const uint32_t allowed = camera->getAllowedVirtualEvents(); + std::vector directEvents; + buildDirectManipulationEvents(allowed, directEvents); + if (directEvents.empty()) + { + outError = "No allowed virtual events for camera \"" + cameraIdentifier + "\"."; + return false; + } + + nbl::system::SCameraManipulationDelta directDelta = {}; + if (!nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { directEvents.data(), directEvents.size() }, directDelta, CameraTinyScalarEpsilon)) + { + outError = "Direct manipulate smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + + { + const auto modifiedPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, "smoke-direct"); + const bool requireDirectPresetChange = camera->getKind() != ICamera::CameraKind::Path; + if (!applyPresetAndValidate( + goalSolver, + camera, + initialPreset, + EPresetComparePolicy::StrictThresholds, + false, + false, + "Preset restore from direct smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + + if (!applyPresetAndValidate( + goalSolver, + camera, + modifiedPreset, + EPresetComparePolicy::StrictThresholds, + requireDirectPresetChange, + false, + "Preset apply from direct smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + + if (!applyPresetAndValidate( + goalSolver, + camera, + initialPreset, + EPresetComparePolicy::StrictThresholds, + false, + false, + "Preset final restore smoke failed for camera \"" + cameraIdentifier + "\"", + outError)) + { + return false; + } + } + + bool keyboardOk = false; + nbl::system::SCameraManipulationDelta keyboardDelta = {}; + for (const auto key : nbl::ui::SCameraInputBindingPhysicalGroups::KeyboardProbeCodes) + { + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); + auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); + if (keyboardEvents.empty()) + continue; + if (nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { keyboardEvents.data(), keyboardEvents.size() }, keyboardDelta, CameraTinyScalarEpsilon)) + { + keyboardOk = true; + break; + } + } + if (!keyboardOk) + { + outError = "Keyboard binding smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + + const auto& mousePreset = CCameraInputBindingUtilities::getDefaultCameraMouseMappingPreset(*camera); + const bool hasMoveMapping = nbl::ui::CCameraInputBindingUtilities::hasMouseRelativeMovementBinding(mousePreset); + const bool hasScrollMapping = nbl::ui::CCameraInputBindingUtilities::hasMouseScrollBinding(mousePreset); + + nbl::system::SCameraManipulationDelta mouseMoveDelta = {}; + if (hasMoveMapping) + { + const auto moveEv = buildMovementSmokeMouseEvent(); + const std::array rawMove = { moveEv }; + auto filteredMoveLookDown = filterOrbitMouseEvents(camera, rawMove, true); + auto filteredMoveLookUp = filterOrbitMouseEvents(camera, rawMove, false); + const bool hasBlockedMovement = std::any_of(filteredMoveLookUp.begin(), filteredMoveLookUp.end(), [](const SMouseEvent& ev) { return ev.type == ui::SMouseEvent::EET_MOVEMENT; }); + if (camera->hasCapability(ICamera::SphericalTarget) && hasBlockedMovement) + { + outError = "Orbit mouse movement gate failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); + auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); + if (mouseMoveEvents.empty()) + { + outError = "Mouse move virtual events missing for camera \"" + cameraIdentifier + "\"."; + return false; + } + if (!nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { mouseMoveEvents.data(), mouseMoveEvents.size() }, mouseMoveDelta, CameraTinyScalarEpsilon)) + { + outError = "Mouse move binding smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + } + + nbl::system::SCameraManipulationDelta mouseScrollDelta = {}; + if (hasScrollMapping) + { + const auto scrollEv = buildScrollSmokeMouseEvent(); + const std::array rawScroll = { scrollEv }; + auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); + + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); + auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); + if (mouseScrollEvents.empty()) + { + outError = "Mouse scroll virtual events missing for camera \"" + cameraIdentifier + "\"."; + return false; + } + if (!nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { mouseScrollEvents.data(), mouseScrollEvents.size() }, mouseScrollDelta, CameraTinyScalarEpsilon)) + { + outError = "Mouse scroll binding smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + } + + std::cout << "[headless-camera-smoke][pass] " << cameraIdentifier + << " direct_pos_delta=" << directDelta.position + << " direct_rot_delta_deg=" << directDelta.rotationDeg + << " kb_pos_delta=" << keyboardDelta.position + << " kb_rot_delta_deg=" << keyboardDelta.rotationDeg + << " mouse_move_pos_delta=" << mouseMoveDelta.position + << " mouse_move_rot_delta_deg=" << mouseMoveDelta.rotationDeg + << " mouse_scroll_pos_delta=" << mouseScrollDelta.position + << " mouse_scroll_rot_delta_deg=" << mouseScrollDelta.rotationDeg + << std::endl; + } + + return true; + } + + inline bool verifyApproximateCrossKindApply( + const CCameraGoalSolver& goalSolver, + ICamera* const targetCamera, + const CameraPreset& sourcePreset, + const CCameraGoalSolver::SApplyResult::EIssue expectedIssue, + const char* const label, + std::string& outError) + { + if (!targetCamera) + return true; + + const ICamera::goal_state_flags_t expectedMissingGoalStateMask(expectedMissingGoalStateMaskForIssue(expectedIssue)); + const auto compatibility = nbl::core::CCameraGoalAnalysisUtilities::analyzePresetApply(goalSolver, targetCamera, sourcePreset).compatibility; + if (compatibility.exact || compatibility.missingGoalStateMask != expectedMissingGoalStateMask) + { + outError = std::string("Cross-kind preset compatibility smoke failed for ") + label + + ". missing=" + CCameraTextUtilities::describeGoalStateMask(compatibility.missingGoalStateMask); + return false; + } + + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, targetCamera, std::string(label) + "-baseline"); + const auto applyResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(goalSolver, targetCamera, sourcePreset); + if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) + { + outError = std::string("Cross-kind preset smoke failed for ") + label + ". " + CCameraTextUtilities::describeApplyResult(applyResult); + return false; + } + + return applyPresetAndValidate( + goalSolver, + targetCamera, + baselinePreset, + EPresetComparePolicy::StrictThresholds, + false, + false, + std::string("Cross-kind preset restore smoke failed for ") + label, + outError); + } + + inline bool verifyExactCrossKindApply( + const CCameraGoalSolver& goalSolver, + ICamera* const targetCamera, + const CameraPreset& sourcePreset, + const char* const label, + std::string& outError) + { + if (!targetCamera) + return true; + + const auto compatibility = nbl::core::CCameraGoalAnalysisUtilities::analyzePresetApply(goalSolver, targetCamera, sourcePreset).compatibility; + if (!compatibility.exact || compatibility.missingGoalStateMask != ICamera::GoalStateNone) + { + outError = std::string("Exact cross-kind preset compatibility smoke failed for ") + label + + ". missing=" + CCameraTextUtilities::describeGoalStateMask(compatibility.missingGoalStateMask); + return false; + } + + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, targetCamera, std::string(label) + "-baseline"); + if (!applyPresetAndValidate( + goalSolver, + targetCamera, + sourcePreset, + EPresetComparePolicy::StrictThresholds, + false, + true, + std::string("Exact cross-kind preset smoke failed for ") + label, + outError)) + { + return false; + } + + return applyPresetAndValidate( + goalSolver, + targetCamera, + baselinePreset, + EPresetComparePolicy::StrictThresholds, + false, + true, + std::string("Exact cross-kind preset restore smoke failed for ") + label, + outError); + } + + inline camera_json_t makeScriptedRuntimeParserSmokeJson() + { + camera_json_t json = { + { "enabled", true }, + { "capture_prefix", SCameraSmokeRuntimeParserDefaults::CapturePrefix }, + { "camera_controls", { + { "keyboard_scale", SCameraSmokeRuntimeParserDefaults::KeyboardScale }, + { "rotation_scale", SCameraSmokeRuntimeParserDefaults::RotationScale } + } }, + { "events", camera_json_t::array({ + camera_json_t{ + { "frame", SCameraSmokeRuntimeParserDefaults::EventFrame }, + { "type", "action" }, + { "action", "set_active_planar" }, + { "value", SCameraSmokeRuntimeParserDefaults::ActivePlanarValue } + }, + camera_json_t{ + { "frame", SCameraSmokeRuntimeParserDefaults::EventFrame }, + { "type", "keyboard" }, + { "key", "W" }, + { "action", "pressed" }, + { "capture", true } + } + }) }, + { "checks", camera_json_t::array({ + camera_json_t{ + { "frame", SCameraSmokeRuntimeParserDefaults::EventFrame }, + { "kind", "baseline" } + }, + camera_json_t{ + { "frame", SCameraSmokeRuntimeParserDefaults::StepFrame }, + { "kind", "gimbal_step" }, + { "min_pos_delta", SCameraSmokeRuntimeParserDefaults::MinPositionDelta }, + { "max_pos_delta", SCameraSmokeRuntimeParserDefaults::MaxPositionDelta } + } + }) } + }; + return json; + } + + inline SCameraFollowVisualMetrics buildFollowVisualMetricsForCamera( + const std::span> planarProjections, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig) + { + nbl::system::SCameraProjectionContext projectionContext = {}; + const bool hasProjectionContext = nbl::ui::tryBuildCameraProjectionContext(planarProjections, camera, projectionContext); + return nbl::system::CCameraFollowRegressionUtilities::buildFollowVisualMetrics( + camera, + trackedTarget, + &followConfig, + hasProjectionContext ? &projectionContext : nullptr); + } + + inline float32_t3x4 buildFollowTargetMarkerWorldForSmoke(const CTrackedTarget& trackedTarget) + { + return buildFollowTargetMarkerWorldTransform( + trackedTarget, + SCameraAppSceneDefaults::FollowTargetMarkerScale); + } + + inline bool verifyFollowTargetContractForSmoke( + const CCameraGoalSolver& goalSolver, + const std::span> planarProjections, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, + const CCameraGoal& followGoal, + const std::string_view label, + std::string& outError) + { + nbl::system::SCameraFollowRegressionResult regression = {}; + std::string regressionError; + nbl::system::SCameraProjectionContext projectionContext = {}; + const bool hasProjectionContext = nbl::ui::tryBuildCameraProjectionContext(planarProjections, camera, projectionContext); + if (nbl::system::CCameraFollowRegressionUtilities::validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + followGoal, + regression, + ®ressionError, + hasProjectionContext ? &projectionContext : nullptr, + CameraFollowRegressionThresholds)) + { + return true; + } + + outError = std::string("Follow smoke validation failed for ") + std::string(label) + ". " + regressionError; + return false; + } + + inline bool verifyFollowTargetMarkerAlignmentForSmoke( + const CTrackedTarget& trackedTarget, + const std::string_view label, + std::string& outError) + { + const auto markerWorld = buildFollowTargetMarkerWorldForSmoke(trackedTarget); + const auto markerTransform = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(markerWorld)); + const auto markerPosition = hlsl::CCameraMathUtilities::castVector(float32_t3(markerTransform[3])); + const auto positionDelta = markerPosition - trackedTarget.getGimbal().getPosition(); + const auto errorLength = length(positionDelta); + if (hlsl::CCameraMathUtilities::isFiniteScalar(errorLength) && errorLength <= CameraTinyScalarEpsilon) + return true; + + outError = std::string("Follow target marker alignment smoke failed for ") + std::string(label) + "."; + return false; + } + + inline bool verifyOffsetFollowRecaptureForSmoke( + const CCameraGoalSolver& goalSolver, + const std::span> planarProjections, + ICamera* const camera, + const CTrackedTarget& trackedTarget, + const std::string_view label, + std::string& outError) + { + if (!camera) + return true; + + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, std::string(label) + " baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::KeepLocalOffset; + + if (!nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(goalSolver, camera, trackedTarget, followConfig)) + { + outError = std::string("Follow recapture smoke failed to capture initial offset for ") + std::string(label) + "."; + return false; + } + + const auto initialApply = nbl::core::CCameraFollowUtilities::applyFollowToCamera(goalSolver, camera, trackedTarget, followConfig); + if (!initialApply.succeeded()) + { + outError = std::string("Follow recapture smoke failed to apply initial follow for ") + std::string(label) + "."; + return false; + } + + auto editedPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, std::string(label) + " edited"); + if (!editedPreset.goal.hasOrbitState) + { + outError = std::string("Follow recapture smoke missing orbit state for ") + std::string(label) + "."; + return false; + } + + editedPreset.goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad( + editedPreset.goal.orbitUv.x + hlsl::radians(SCameraSmokeFollowScenario::OrbitRecaptureDeltaDeg)); + editedPreset.goal.orbitDistance = std::clamp( + editedPreset.goal.orbitDistance + SCameraSmokeFollowScenario::OrbitRecaptureDistanceDelta, + CSphericalTargetCamera::MinDistance, + CSphericalTargetCamera::MaxDistance); + editedPreset.goal = nbl::core::CCameraGoalUtilities::canonicalizeGoal(editedPreset.goal); + if (!nbl::core::CCameraGoalUtilities::isGoalFinite(editedPreset.goal)) + { + outError = std::string("Follow recapture smoke produced a non-finite edited goal for ") + std::string(label) + "."; + return false; + } + + const auto editedApply = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(goalSolver, camera, editedPreset); + if (!editedApply.succeeded() || !editedApply.changed()) + { + outError = std::string("Follow recapture smoke failed to apply edited preset for ") + std::string(label) + + ". " + CCameraTextUtilities::describeApplyResult(editedApply); + return false; + } + + const auto reachedEditedPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, std::string(label) + " reached"); + + if (!nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(goalSolver, camera, trackedTarget, followConfig)) + { + outError = std::string("Follow recapture smoke failed to recapture offset for ") + std::string(label) + "."; + return false; + } + + CCameraGoal recapturedGoal = {}; + if (!nbl::core::CCameraFollowUtilities::tryBuildFollowGoal(goalSolver, camera, trackedTarget, followConfig, recapturedGoal)) + { + outError = std::string("Follow recapture smoke failed to rebuild follow goal for ") + std::string(label) + "."; + return false; + } + + const auto recapturedApply = nbl::core::CCameraFollowUtilities::applyFollowToCamera(goalSolver, camera, trackedTarget, followConfig); + if (!recapturedApply.succeeded()) + { + outError = std::string("Follow recapture smoke failed to apply recaptured follow for ") + std::string(label) + + ". " + CCameraTextUtilities::describeApplyResult(recapturedApply); + return false; + } + + if (!nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, reachedEditedPreset)) + { + outError = std::string("Follow recapture smoke mismatch for ") + std::string(label) + ". " + + nbl::core::CCameraPresetFlowUtilities::describePresetCameraMismatch(goalSolver, camera, reachedEditedPreset); + return false; + } + + if (!verifyFollowTargetContractForSmoke(goalSolver, planarProjections, camera, trackedTarget, followConfig, recapturedGoal, label, outError)) + return false; + + return restorePresetStrict( + goalSolver, + camera, + baselinePreset, + "Follow recapture smoke failed to restore baseline for " + std::string(label), + outError); + } + + inline bool verifyScriptedRuntimeFrameBatch(std::string* const outError) + { + CCameraScriptedTimeline timeline = {}; + std::vector actionEvents; + nbl::this_example::CCameraScriptedActionUtilities::appendActionEvent( + actionEvents, + SCameraSmokeRuntimeDefaults::ActionFrame, + nbl::this_example::ECameraScriptedActionCode::SetActivePlanar, + SCameraSmokeRuntimeDefaults::ActivePlanarValue); + { + CCameraGoal goal = {}; + goal.position = SCameraSmokeRuntimeDefaults::GoalPosition; + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent(timeline, SCameraSmokeRuntimeDefaults::ActionFrame, goal, true); + } + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedSegmentLabelEvent( + timeline, + SCameraSmokeRuntimeDefaults::ActionFrame, + std::string(SCameraSmokeRuntimeDefaults::SegmentLabel)); + { + float64_t4x4 transform = float64_t4x4(1.0); + transform[3] = float64_t4(SCameraSmokeRuntimeDefaults::TrackedTargetPosition, 1.0); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, SCameraSmokeRuntimeDefaults::FollowFrame, transform); + } + nbl::this_example::CCameraScriptedActionUtilities::finalizeActionEvents(actionEvents); + + size_t nextEventIndex = 0u; + size_t nextActionIndex = 0u; + CCameraScriptedFrameEvents batch; + std::vector actions; + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::ActionFrame, batch); + nbl::this_example::CCameraScriptedActionUtilities::dequeueFrameActions(actionEvents, nextActionIndex, SCameraSmokeRuntimeDefaults::ActionFrame, actions); + if (nextEventIndex != 2u || actions.size() != 1u || batch.goals.size() != 1u || + batch.segmentLabels.size() != 1u || !batch.mouse.empty() || !batch.keyboard.empty()) + { + if (outError) + *outError = "Scripted runtime frame batch smoke failed for frame 3."; + return false; + } + if (!nbl::this_example::CCameraScriptedActionUtilities::hasCode(actions.front(), nbl::this_example::ECameraScriptedActionCode::SetActivePlanar) || + actions.front().value != SCameraSmokeRuntimeDefaults::ActivePlanarValue || + batch.segmentLabels.front() != SCameraSmokeRuntimeDefaults::SegmentLabel) + { + if (outError) + *outError = "Scripted runtime frame batch payload smoke failed for frame 3."; + return false; + } + + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::FollowFrame, batch); + nbl::this_example::CCameraScriptedActionUtilities::dequeueFrameActions(actionEvents, nextActionIndex, SCameraSmokeRuntimeDefaults::FollowFrame, actions); + if (nextEventIndex != timeline.events.size() || batch.trackedTargetTransforms.size() != 1u || + !actions.empty() || !batch.goals.empty()) + { + if (outError) + *outError = "Scripted runtime frame batch smoke failed for frame 4."; + return false; + } + const auto trackedTargetPosition = float64_t3(batch.trackedTargetTransforms.front().transform[3]); + if (!hlsl::CCameraMathUtilities::nearlyEqualVec3(trackedTargetPosition, SCameraSmokeRuntimeDefaults::TrackedTargetPosition, CameraTinyScalarEpsilon)) + { + if (outError) + *outError = "Scripted runtime tracked-target payload smoke failed."; + return false; + } + + return true; + } + + inline bool verifyScriptedRuntimeParser(std::string* const outError) + { + nbl::this_example::CCameraScriptedInputParseResult parsed; + std::string parseError; + const std::string scriptText = makeScriptedRuntimeParserSmokeJson().dump(); + if (!nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::readCameraScriptedInput(scriptText, parsed, &parseError)) + { + if (outError) + *outError = "Scripted runtime parser smoke failed to parse low-level runtime payload. " + parseError; + return false; + } + if (!parsed.enabled || + parsed.capturePrefix != SCameraSmokeRuntimeParserDefaults::CapturePrefix || + !parsed.cameraControls.hasKeyboardScale || + !parsed.cameraControls.hasRotationScale) + { + if (outError) + *outError = "Scripted runtime parser smoke lost top-level metadata."; + return false; + } + if (parsed.timeline.events.size() != 1u || parsed.actionEvents.size() != 1u || parsed.timeline.checks.size() != 2u || parsed.timeline.captureFrames.size() != 1u) + { + if (outError) + *outError = "Scripted runtime parser smoke produced wrong payload counts."; + return false; + } + if (parsed.timeline.captureFrames.front() != SCameraSmokeRuntimeParserDefaults::EventFrame) + { + if (outError) + *outError = "Scripted runtime parser smoke produced wrong capture frame."; + return false; + } + + size_t nextEventIndex = 0u; + size_t nextActionIndex = 0u; + CCameraScriptedFrameEvents batch; + std::vector actions; + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, batch); + nbl::this_example::CCameraScriptedActionUtilities::dequeueFrameActions(parsed.actionEvents, nextActionIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, actions); + if (actions.size() != 1u || + batch.keyboard.size() != 1u || + actions.front().value != SCameraSmokeRuntimeParserDefaults::ActivePlanarValue) + { + if (outError) + *outError = "Scripted runtime parser smoke produced wrong frame-two batch."; + return false; + } + if (parsed.timeline.checks.front().kind != CCameraScriptedInputCheck::Kind::Baseline || + parsed.timeline.checks.back().kind != CCameraScriptedInputCheck::Kind::GimbalStep) + { + if (outError) + *outError = "Scripted runtime parser smoke produced wrong check kinds."; + return false; + } + + return true; + } + + inline bool verifyScriptedCheckRunner(const CCameraGoalSolver& goalSolver, std::string* const outError) + { + auto orbitCamera = core::make_smart_refctd_ptr( + SCameraSmokeScriptedCheckDefaults::OrbitCameraPosition, + SCameraSmokeScriptedCheckDefaults::OrbitCameraTarget); + CTrackedTarget trackedTarget( + SCameraSmokeScriptedCheckDefaults::InitialTrackedTargetPosition, + SCameraSmokeScriptedCheckDefaults::InitialTrackedTargetOrientation); + + CCameraScriptedTimeline timeline = {}; + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, SCameraSmokeScriptedCheckDefaults::BaselineFrame); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedGimbalStepCheck( + timeline, + SCameraSmokeScriptedCheckDefaults::StepFrame, + true, + SCameraSmokeScriptedCheckDefaults::PositionTolerance, + SCameraSmokeScriptedCheckDefaults::MinPositionDelta, + true, + SCameraSmokeScriptedCheckDefaults::AngularToleranceDeg, + SCameraSmokeScriptedCheckDefaults::MinAngularDeltaDeg); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck( + timeline, + SCameraSmokeScriptedCheckDefaults::FollowLockFrame, + CameraFollowRegressionThresholds.lockAngleToleranceDeg, + CameraFollowRegressionThresholds.projectedNdcTolerance); + + CCameraScriptedCheckRuntimeState state = {}; + { + const auto frameResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( + timeline.checks, + state, + { + .frame = SCameraSmokeScriptedCheckDefaults::BaselineFrame, + .camera = orbitCamera.get() + }); + if (frameResult.hadFailures || state.nextCheckIndex != 1u || !state.baseline.valid || !state.step.valid) + { + const auto& gimbal = orbitCamera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto orientation = gimbal.getOrientation(); + const auto basis = gimbal.getOrthonornalMatrix(); + const auto eulerDeg = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + std::ostringstream oss; + oss << std::fixed << std::setprecision(6) + << "Scripted check runner baseline smoke failed." + << " nextCheckIndex=" << state.nextCheckIndex + << " baselineValid=" << state.baseline.valid + << " stepValid=" << state.step.valid + << " pos=(" << pos.x << ", " << pos.y << ", " << pos.z << ")" + << " quat=(" << orientation.data.x << ", " << orientation.data.y << ", " << orientation.data.z << ", " << orientation.data.w << ")" + << " basis_x=(" << basis[0].x << ", " << basis[0].y << ", " << basis[0].z << ")" + << " basis_y=(" << basis[1].x << ", " << basis[1].y << ", " << basis[1].z << ")" + << " basis_z=(" << basis[2].x << ", " << basis[2].y << ", " << basis[2].z << ")" + << " euler_deg=(" << eulerDeg.x << ", " << eulerDeg.y << ", " << eulerDeg.z << ")"; + if (!frameResult.logs.empty()) + oss << ' ' << frameResult.logs.front().text; + if (outError) + *outError = oss.str(); + return false; + } + } + + { + CVirtualGimbalEvent stepEvent = {}; + stepEvent.type = CVirtualGimbalEvent::MoveRight; + stepEvent.magnitude = SCameraSmokeScriptedCheckDefaults::StepEventMagnitude; + if (!orbitCamera->manipulate({ &stepEvent, 1u })) + { + if (outError) + *outError = "Scripted check runner smoke failed to manipulate the camera for step validation."; + return false; + } + + const auto frameResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( + timeline.checks, + state, + { + .frame = SCameraSmokeScriptedCheckDefaults::StepFrame, + .camera = orbitCamera.get() + }); + if (frameResult.hadFailures || state.nextCheckIndex != 2u) + { + if (outError) + *outError = std::string("Scripted check runner step smoke failed. ") + + (!frameResult.logs.empty() ? frameResult.logs.front().text : std::string("missing log details")); + return false; + } + } + + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::OrbitTarget; + CCameraGoal followGoal = {}; + if (!nbl::core::CCameraFollowUtilities::applyFollowToCamera(goalSolver, orbitCamera.get(), trackedTarget, followConfig, &followGoal).succeeded()) + { + if (outError) + *outError = "Scripted check runner smoke failed to apply follow before follow-lock validation."; + return false; + } + + { + const auto frameResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( + timeline.checks, + state, + { + .frame = SCameraSmokeScriptedCheckDefaults::FollowLockFrame, + .camera = orbitCamera.get(), + .trackedTarget = &trackedTarget, + .followConfig = &followConfig, + .goalSolver = &goalSolver + }); + if (frameResult.hadFailures || state.nextCheckIndex != timeline.checks.size()) + { + const auto details = !frameResult.logs.empty() ? frameResult.logs.front().text : std::string("missing log details"); + const auto& gimbal = orbitCamera->getGimbal(); + const auto cameraPos = gimbal.getPosition(); + const auto cameraForward = gimbal.getZAxis(); + const auto targetPos = trackedTarget.getGimbal().getPosition(); + const auto desiredForward = normalize(targetPos - cameraPos); + camera_quaternion_t desiredOrientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + if (!nbl::hlsl::CCameraMathUtilities::tryBuildLookAtOrientation( + cameraPos, + targetPos, + float64_t3(0.0, 1.0, 0.0), + desiredOrientation)) + { + if (outError) + *outError = "Scripted check runner follow-lock smoke failed to build desired look-at orientation."; + return false; + } + const auto desiredBasis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(desiredOrientation); + const auto desiredRight = desiredBasis[0]; + const auto desiredUp = desiredBasis[1]; + const auto goalRightVec = hlsl::CCameraMathUtilities::normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(1.0, 0.0, 0.0), true); + const auto goalUpVec = hlsl::CCameraMathUtilities::normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 1.0, 0.0), true); + const auto goalForwardVec = hlsl::CCameraMathUtilities::normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 0.0, 1.0), true); + const auto goalBasis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(followGoal.orientation); + float lockAngle = 0.0f; + double targetDistance = 0.0; + const bool hasLockMetrics = nbl::core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(gimbal, trackedTarget, lockAngle, &targetDistance); + std::ostringstream oss; + oss << std::fixed << std::setprecision(6) + << "Scripted check runner follow-lock smoke failed. " << details + << " camera_pos=(" << cameraPos.x << ", " << cameraPos.y << ", " << cameraPos.z << ")" + << " camera_forward=(" << cameraForward.x << ", " << cameraForward.y << ", " << cameraForward.z << ")" + << " target_pos=(" << targetPos.x << ", " << targetPos.y << ", " << targetPos.z << ")" + << " desired_forward=(" << desiredForward.x << ", " << desiredForward.y << ", " << desiredForward.z << ")" + << " desired_right=(" << desiredRight.x << ", " << desiredRight.y << ", " << desiredRight.z << ")" + << " desired_up=(" << desiredUp.x << ", " << desiredUp.y << ", " << desiredUp.z << ")" + << " goal_pos=(" << followGoal.position.x << ", " << followGoal.position.y << ", " << followGoal.position.z << ")" + << " goal_quat=(" << followGoal.orientation.data.x << ", " << followGoal.orientation.data.y << ", " + << followGoal.orientation.data.z << ", " << followGoal.orientation.data.w << ")" + << " goal_right_vec=(" << goalRightVec.x << ", " << goalRightVec.y << ", " << goalRightVec.z << ")" + << " goal_up_vec=(" << goalUpVec.x << ", " << goalUpVec.y << ", " << goalUpVec.z << ")" + << " goal_forward_vec=(" << goalForwardVec.x << ", " << goalForwardVec.y << ", " << goalForwardVec.z << ")" + << " goal_basis_x=(" << goalBasis[0].x << ", " << goalBasis[0].y << ", " << goalBasis[0].z << ")" + << " goal_basis_y=(" << goalBasis[1].x << ", " << goalBasis[1].y << ", " << goalBasis[1].z << ")" + << " goal_basis_z=(" << goalBasis[2].x << ", " << goalBasis[2].y << ", " << goalBasis[2].z << ")"; + if (hasLockMetrics) + oss << " lock_angle_deg=" << lockAngle << " target_distance=" << targetDistance; + if (outError) + *outError = oss.str(); + return false; + } + } + + return true; + } + + diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp new file mode 100644 index 000000000..2d701435f --- /dev/null +++ b/61_UI/AppImGuiListen.cpp @@ -0,0 +1,22 @@ +#include "app/App.hpp" + +void App::imguiListen() +{ + ImGuiIO& io = ImGui::GetIO(); + if (m_cliRuntime.ciMode) + io.IniFilename = nullptr; + + ImGuizmo::BeginFrame(); + + auto info = SCameraAppUiTextureSlots::makeDefaultViewportResourceInfo(); + + if (m_viewports.useWindow) + drawWindowedViewportWindows(io, info); + else + drawFullscreenViewportWindow(io, info); + + drawScriptVisualDebugOverlay(io.DisplaySize); + DrawControlPanel(); + finalizeUiFrameState(); +} + diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp new file mode 100644 index 000000000..74ee23e4a --- /dev/null +++ b/61_UI/AppInit.cpp @@ -0,0 +1,81 @@ +#include "app/App.hpp" +#include "app/AppResourceUtilities.hpp" + +bool App::onAppInitialized(smart_refctd_ptr&& system) +{ + argparse::ArgumentParser program("Virtual camera event system demo"); + + program.add_argument("--file") + .help("Path to json file with camera inputs"); + program.add_argument("--ci") + .help("Run in CI mode: capture a screenshot after a few frames and exit.") + .default_value(false) + .implicit_value(true); + program.add_argument("--script") + .help("Path to json file with scripted input events"); + program.add_argument("--script-log") + .help("Log scripted input and virtual events.") + .default_value(false) + .implicit_value(true); + program.add_argument("--script-visual-debug") + .help("Enable scripted visual debug overlay and fixed frame pacing.") + .default_value(false) + .implicit_value(true); + program.add_argument("--no-screenshots") + .help("Disable CI and scripted screenshot captures.") + .default_value(false) + .implicit_value(true); + program.add_argument("--headless-camera-smoke") + .help("Run a headless camera-only smoke test and exit after initialization.") + .default_value(false) + .implicit_value(true); + + try + { + program.parse_args({ argv.data(), argv.data() + argv.size() }); + } + catch (const std::exception& err) + { + std::cerr << err.what() << std::endl << program; + return false; + } + + m_cliRuntime.headlessCameraSmokeMode = program.get("--headless-camera-smoke"); + if (m_cliRuntime.headlessCameraSmokeMode) + return runHeadlessCameraSmoke(program, std::move(system)); + + m_cliRuntime.ciMode = program.get("--ci"); + if (m_cliRuntime.ciMode) + { + m_cliRuntime.ciScreenshotPath = localOutputCWD / "cameraz_ci.png"; + m_cliRuntime.ciStartedAt = clock_t::now(); + m_viewports.useWindow = true; + } + m_scriptedInput.log = program.get("--script-log"); + m_cliRuntime.scriptVisualDebugCli = program.get("--script-visual-debug"); + m_cliRuntime.disableScreenshotsCli = program.get("--no-screenshots"); + + m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + m_logFormatter = core::make_smart_refctd_ptr(); + + if (!base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; + if (!initializeMountedCameraResources(std::move(system))) + return false; + + if (!initializeCameraConfiguration(program)) + return false; + if (!initializePresentationResources()) + return false; + if (!initializeUiResources()) + return false; + if (!initializeSceneResources()) + return false; + + oracle.reportBeginFrameRecord(); + + if (base_t::argv.size() >= 3 && argv[1] == "-timeout_seconds") + timeout = std::chrono::seconds(std::atoi(argv[2].c_str())); + start = clock_t::now(); + return true; +} diff --git a/61_UI/AppInputRuntime.cpp b/61_UI/AppInputRuntime.cpp new file mode 100644 index 000000000..a403a62a6 --- /dev/null +++ b/61_UI/AppInputRuntime.cpp @@ -0,0 +1,231 @@ +#include "app/App.hpp" + +#include + +struct SCollectedCameraVirtualEvents final +{ + std::vector events = {}; + uint32_t keyboardVirtualEventCount = 0u; + + inline uint32_t totalCount() const + { + return static_cast(events.size()); + } + + inline bool empty() const + { + return events.empty(); + } +}; + +template +inline void appendUniqueCameraInputTargets( + std::span> planarProjections, + std::span windowBindings, + const SActiveViewportRuntimeState& activeViewport, + const bool mirrorInput, + AddTarget&& addTarget) +{ + if (!mirrorInput) + { + addTarget({ + .camera = activeViewport.camera, + .planarIx = activeViewport.requireBinding().activePlanarIx + }); + return; + } + + std::unordered_set visited; + for (const auto& windowBinding : windowBindings) + { + if (windowBinding.activePlanarIx >= planarProjections.size()) + continue; + + const auto& planarProjection = planarProjections[windowBinding.activePlanarIx]; + if (!planarProjection) + continue; + + auto* target = planarProjection->getCamera(); + if (!target || !visited.insert(target).second) + continue; + + addTarget({ + .camera = target, + .planarIx = windowBinding.activePlanarIx + }); + } +} + +inline std::span buildOrbitFilteredMouseInput( + std::span mouseEvents, + const bool orbitLookDown, + std::vector& filteredMouseEvents) +{ + if (orbitLookDown) + return mouseEvents; + + filteredMouseEvents.clear(); + filteredMouseEvents.reserve(mouseEvents.size()); + for (const auto& event : mouseEvents) + { + if (event.type != ui::SMouseEvent::EET_MOVEMENT) + filteredMouseEvents.emplace_back(event); + } + return { filteredMouseEvents.data(), filteredMouseEvents.size() }; +} + +inline void scaleCollectedVirtualEvents( + SCollectedCameraVirtualEvents& virtualEvents, + const CameraControlSettings& cameraControls) +{ + for (uint32_t i = 0u; i < virtualEvents.keyboardVirtualEventCount; ++i) + virtualEvents.events[i].magnitude *= cameraControls.keyboardScale; + + nbl::core::CCameraManipulationUtilities::scaleVirtualEvents( + virtualEvents.events, + virtualEvents.totalCount(), + cameraControls.translationScale, + cameraControls.rotationScale); +} + +template +inline SCollectedCameraVirtualEvents collectActiveCameraVirtualEvents( + SWindowControlBinding& binding, + ICamera* camera, + const std::span keyboardEvents, + const std::span mouseEvents, + const std::chrono::microseconds presentationTimestamp, + const CameraControlSettings& cameraControls, + const bool orbitLikeCamera, + const bool orbitLookDown, + SyncWindowInputBinding&& syncWindowInputBinding) +{ + SCollectedCameraVirtualEvents collectedVirtualEvents = {}; + if (!camera) + return collectedVirtualEvents; + + syncWindowInputBinding(binding); + auto& inputBinder = binding.inputBinding; + + std::vector filteredOrbitMouseEvents; + auto filteredMouseInput = mouseEvents; + if (orbitLikeCamera) + filteredMouseInput = buildOrbitFilteredMouseInput(mouseEvents, orbitLookDown, filteredOrbitMouseEvents); + + auto binderEvents = inputBinder.collectVirtualEvents(presentationTimestamp, { + .keyboardEvents = keyboardEvents, + .mouseEvents = filteredMouseInput + }); + const uint32_t virtualEventCount = binderEvents.totalCount(); + if (!virtualEventCount) + return collectedVirtualEvents; + + collectedVirtualEvents.keyboardVirtualEventCount = binderEvents.keyboardCount; + collectedVirtualEvents.events.assign( + binderEvents.events.begin(), + binderEvents.events.begin() + virtualEventCount); + scaleCollectedVirtualEvents(collectedVirtualEvents, cameraControls); + return collectedVirtualEvents; +} + +template +inline void applyCollectedVirtualEventsToCamera( + ICamera* target, + const uint32_t planarIx, + const SCollectedCameraVirtualEvents& collectedVirtualEvents, + const bool worldTranslate, + const nbl::core::CCameraGoalSolver& goalSolver, + const SCameraConstraintSettings& cameraConstraints, + const bool scriptedInputEnabled, + RefreshFollowOffsets&& refreshFollowOffsets, + AppendVirtualEventLog&& appendVirtualEventLog) +{ + if (!target || collectedVirtualEvents.empty()) + return; + + if (worldTranslate) + { + std::vector perCameraEvents = collectedVirtualEvents.events; + uint32_t perCount = collectedVirtualEvents.totalCount(); + nbl::core::CCameraManipulationUtilities::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); + if (perCount) + target->manipulate({ perCameraEvents.data(), perCount }); + } + else + { + target->manipulate({ collectedVirtualEvents.events.data(), collectedVirtualEvents.totalCount() }); + } + + nbl::this_example::CCameraConstraintUtilities::applyCameraConstraints(goalSolver, target, cameraConstraints); + if (!scriptedInputEnabled) + refreshFollowOffsets(planarIx); + appendVirtualEventLog(target, planarIx, collectedVirtualEvents); +} + +void App::applyActiveCameraInput( + std::span keyboardEvents, + std::span mouseEvents, + const bool skipCameraInput) +{ + if (!(m_viewports.enableActiveCameraMovement && !skipCameraInput)) + return; + + SActiveCameraInputContext inputContext = {}; + if (!tryBuildActiveCameraInputContext(inputContext)) + return; + auto& binding = *inputContext.viewport.binding; + auto* camera = inputContext.viewport.camera; + const bool orbitLookDown = ImGui::IsMouseDown(ImGuiMouseButton_Right) || + (m_scriptedInput.enabled && (m_scriptedInput.scriptedMouseButtons.leftDown || m_scriptedInput.scriptedMouseButtons.rightDown)); + SCollectedCameraVirtualEvents virtualEvents = collectActiveCameraVirtualEvents( + binding, + camera, + keyboardEvents, + mouseEvents, + m_nextPresentationTimestamp, + m_cameraControls, + isOrbitLikeCamera(camera), + orbitLookDown, + [this](SWindowControlBinding& windowBinding) { syncWindowInputBinding(windowBinding); }); + + if (virtualEvents.empty()) + return; + + const auto applyVirtualEventsToCamera = [&](ICamera* target, const uint32_t planarIx) -> void + { + applyCollectedVirtualEventsToCamera( + target, + planarIx, + virtualEvents, + m_cameraControls.worldTranslate, + m_cameraGoalSolver, + m_cameraConstraints, + m_scriptedInput.enabled, + [this](const uint32_t ix) { refreshFollowOffsetConfigForPlanar(ix); }, + [this](ICamera* logCamera, const uint32_t ix, const SCollectedCameraVirtualEvents& collectedEvents) + { + appendVirtualEventLog("input", "Keyboard/Mouse", ix, logCamera, collectedEvents.events.data(), collectedEvents.totalCount()); + }); + }; + + appendUniqueCameraInputTargets( + getPlanarProjectionSpan(), + std::span(m_viewports.windowBindings.data(), m_viewports.windowBindings.size()), + inputContext.viewport, + m_cameraControls.mirrorInput, + [&](const SActiveCameraInputTarget& target) + { + if (!target.valid()) + return; + applyVirtualEventsToCamera(target.camera, target.planarIx); + }); + + if (!m_scriptedInput.log) + return; + + for (const auto& event : virtualEvents.events) + { + m_logger->log("[script] virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(event.type).data(), event.magnitude); + } + logScriptedCameraPose("input", camera); +} diff --git a/61_UI/AppManipulableObjects.cpp b/61_UI/AppManipulableObjects.cpp new file mode 100644 index 000000000..36b494469 --- /dev/null +++ b/61_UI/AppManipulableObjects.cpp @@ -0,0 +1,225 @@ +#include "app/App.hpp" + +inline float32_t4x4 buildModelManipulationTransform(const float32_t3x4& model) +{ + return hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); +} + +inline float32_t3 extractWorldPosition(const float32_t4x4& transform) +{ + return float32_t3(transform[3].x, transform[3].y, transform[3].z); +} + +inline float32_t4x4 buildCameraManipulationTransform(ICamera& camera) +{ + return getCastedMatrix(camera.getGimbal().template operator()()); +} + +inline float32_t3 buildCameraWorldPosition(ICamera& camera) +{ + return hlsl::CCameraMathUtilities::castVector(camera.getGimbal().getPosition()); +} + +inline float32_t4x4 buildFollowTargetTransform(const CTrackedTarget& trackedTarget) +{ + return getCastedMatrix(trackedTarget.getGimbal().template operator()()); +} + +inline float32_t3 buildFollowTargetWorldPosition(const CTrackedTarget& trackedTarget) +{ + return hlsl::CCameraMathUtilities::castVector(trackedTarget.getGimbal().getPosition()); +} + +uint32_t App::getManipulableObjectCount() const +{ + return SCameraAppSceneDefaults::CameraObjectIxOffset + static_cast(m_planarProjections.size()); +} + +bool App::isManipulableObjectFollowTarget(const uint32_t objectIx) const +{ + return objectIx == SCameraAppSceneDefaults::FollowTargetObjectIx; +} + +std::optional App::getManipulableObjectPlanarIx(const uint32_t objectIx) const +{ + if (objectIx < SCameraAppSceneDefaults::CameraObjectIxOffset) + return std::nullopt; + + const auto planarIx = objectIx - SCameraAppSceneDefaults::CameraObjectIxOffset; + if (planarIx >= m_planarProjections.size()) + return std::nullopt; + return planarIx; +} + +bool App::tryBuildManipulableObjectContext(const uint32_t objectIx, SManipulableObjectContext& outContext) const +{ + outContext = {}; + outContext.objectIx = objectIx; + + if (objectIx == SCameraAppSceneDefaults::ModelObjectIx) + { + const auto modelTransform = buildModelManipulationTransform(m_sceneInteraction.model); + outContext.kind = SceneManipulatedObjectKind::Model; + outContext.label = "Model"; + outContext.transform = modelTransform; + outContext.worldPosition = extractWorldPosition(modelTransform); + return true; + } + + if (isManipulableObjectFollowTarget(objectIx)) + { + outContext.kind = SceneManipulatedObjectKind::FollowTarget; + outContext.label = m_sceneInteraction.followTarget.getIdentifier(); + outContext.transform = buildFollowTargetTransform(m_sceneInteraction.followTarget); + outContext.worldPosition = buildFollowTargetWorldPosition(m_sceneInteraction.followTarget); + return true; + } + + const auto planarIx = getManipulableObjectPlanarIx(objectIx); + if (!planarIx.has_value()) + return false; + + auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; + if (!camera) + return false; + + outContext.kind = SceneManipulatedObjectKind::Camera; + outContext.planarIx = planarIx; + outContext.camera = camera; + outContext.label = std::string(CCameraTextUtilities::getCameraTypeLabel(camera)) + " Camera"; + outContext.transform = buildCameraManipulationTransform(*camera); + outContext.worldPosition = buildCameraWorldPosition(*camera); + return true; +} + +bool App::tryBuildActiveManipulatedObjectContext(SManipulableObjectContext& outContext) const +{ + return tryBuildManipulableObjectContext(getManipulatedObjectIx(), outContext); +} + +uint32_t App::getManipulatedObjectIx() const +{ + switch (m_sceneInteraction.manipulatedObjectKind) + { + case SceneManipulatedObjectKind::Model: + return SCameraAppSceneDefaults::ModelObjectIx; + case SceneManipulatedObjectKind::FollowTarget: + return SCameraAppSceneDefaults::FollowTargetObjectIx; + case SceneManipulatedObjectKind::Camera: + default: + return m_sceneInteraction.boundPlanarCameraIxToManipulate.has_value() ? + (m_sceneInteraction.boundPlanarCameraIxToManipulate.value() + SCameraAppSceneDefaults::CameraObjectIxOffset) : + SCameraAppSceneDefaults::ModelObjectIx; + } +} + +void App::bindManipulatedModel() +{ + m_sceneInteraction.manipulatedObjectKind = SceneManipulatedObjectKind::Model; + m_sceneInteraction.boundCameraToManipulate = nullptr; + m_sceneInteraction.boundPlanarCameraIxToManipulate = std::nullopt; +} + +void App::bindManipulatedFollowTarget() +{ + m_sceneInteraction.manipulatedObjectKind = SceneManipulatedObjectKind::FollowTarget; + m_sceneInteraction.boundCameraToManipulate = nullptr; + m_sceneInteraction.boundPlanarCameraIxToManipulate = std::nullopt; +} + +void App::bindManipulatedCamera(const uint32_t planarIx) +{ + if (planarIx >= m_planarProjections.size()) + { + bindManipulatedModel(); + return; + } + + auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + if (!camera) + { + bindManipulatedModel(); + return; + } + + m_sceneInteraction.manipulatedObjectKind = SceneManipulatedObjectKind::Camera; + m_sceneInteraction.boundPlanarCameraIxToManipulate = planarIx; + m_sceneInteraction.boundCameraToManipulate = smart_refctd_ptr(camera); +} + +void App::bindManipulableObject(const SManipulableObjectContext& context) +{ + switch (context.kind) + { + case SceneManipulatedObjectKind::Model: + bindManipulatedModel(); + break; + case SceneManipulatedObjectKind::FollowTarget: + bindManipulatedFollowTarget(); + break; + case SceneManipulatedObjectKind::Camera: + if (context.planarIx.has_value()) + bindManipulatedCamera(context.planarIx.value()); + else + bindManipulatedModel(); + break; + } +} + +void App::bindManipulatedObjectByIx(const uint32_t objectIx) +{ + SManipulableObjectContext context = {}; + if (!tryBuildManipulableObjectContext(objectIx, context)) + { + bindManipulatedModel(); + return; + } + + bindManipulableObject(context); +} + +std::string App::getManipulableObjectLabel(const uint32_t objectIx) const +{ + SManipulableObjectContext context = {}; + if (!tryBuildManipulableObjectContext(objectIx, context)) + return "Unknown"; + return context.label; +} + +float32_t4x4 App::getManipulableObjectTransform(const uint32_t objectIx) const +{ + SManipulableObjectContext context = {}; + if (!tryBuildManipulableObjectContext(objectIx, context)) + return float32_t4x4(1.0f); + return context.transform; +} + +float32_t3 App::getManipulableObjectWorldPosition(const uint32_t objectIx) const +{ + SManipulableObjectContext context = {}; + if (!tryBuildManipulableObjectContext(objectIx, context)) + return float32_t3(0.0f); + return context.worldPosition; +} + +void App::applyManipulableObjectTransform(const SManipulableObjectContext& context, const float64_t4x4& transform) +{ + switch (context.kind) + { + case SceneManipulatedObjectKind::Camera: + if (context.camera) + { + nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(context.camera, transform); + if (context.planarIx.has_value()) + refreshFollowOffsetConfigForPlanar(context.planarIx.value()); + } + break; + case SceneManipulatedObjectKind::FollowTarget: + setFollowTargetTransform(transform); + applyFollowToConfiguredCameras(); + break; + case SceneManipulatedObjectKind::Model: + m_sceneInteraction.model = float32_t3x4(hlsl::transpose(getCastedMatrix(transform))); + break; + } +} diff --git a/61_UI/AppPresentationResources.cpp b/61_UI/AppPresentationResources.cpp new file mode 100644 index 000000000..61efc2264 --- /dev/null +++ b/61_UI/AppPresentationResources.cpp @@ -0,0 +1,164 @@ +#include "app/App.hpp" + +nbl::hlsl::uint32_t2 App::getPresentationRenderExtent() const +{ + if (m_cliRuntime.ciMode && !m_cliRuntime.scriptVisualDebugCli) + return SCameraAppPresentationDefaults::CiWindowExtent; + + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + return nbl::hlsl::uint32_t2(dpyInfo.resX, dpyInfo.resY); +} + +bool App::shouldMaximizePresentationWindow() const +{ + return !m_cliRuntime.ciMode || m_cliRuntime.scriptVisualDebugCli; +} + +core::vector App::getSurfaces() const +{ + if (!m_surface) + { + const auto presentationExtent = getPresentationRenderExtent(); + auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + + IWindow::SCreationParams params = {}; + params.callback = windowCallback; + params.width = presentationExtent.x; + params.height = presentationExtent.y; + params.x = SCameraAppPresentationDefaults::WindowOrigin.x; + params.y = SCameraAppPresentationDefaults::WindowOrigin.y; + params.flags = IWindow::ECF_INPUT_FOCUS | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE | IWindow::ECF_CAN_MINIMIZE; + params.windowCaption = "[Nabla Engine] UI App"; + + const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); + auto surface = CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); + const_cast&>(m_surface) = CSmoothResizeSurface::create(std::move(surface)); + } + + if (!m_surface) + return {}; + + if (shouldMaximizePresentationWindow()) + m_window->getManager()->maximize(m_window.get()); + m_window->getCursorControl()->setVisible(true); + return { {m_surface->getSurface()} }; +} + +bool App::initializePresentationResources() +{ + m_semaphore = m_device->createSemaphore(m_realFrameIx); + if (!m_semaphore) + return logFail("Failed to Create a Semaphore!"); + + const auto format = asset::EF_R8G8B8A8_SRGB; + const auto samples = IGPUImage::ESCF_1_BIT; + + { + IGPURenderpass::SCreationParams params = {}; + const IGPURenderpass::SCreationParams::SColorAttachmentDescription colorAttachments[] = { + {{ + { + .format = format, + .samples = samples, + .mayAlias = false + }, + /*.loadOp = */IGPURenderpass::LOAD_OP::CLEAR, + /*.storeOp = */IGPURenderpass::STORE_OP::STORE, + /*.initialLayout = */IGPUImage::LAYOUT::UNDEFINED, + /*.finalLayout = */ IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL + }}, + IGPURenderpass::SCreationParams::ColorAttachmentsEnd + }; + params.colorAttachments = colorAttachments; + IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { + {}, + IGPURenderpass::SCreationParams::SubpassesEnd + }; + subpasses[0].colorAttachments[0] = { .render = { .attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL } }; + params.subpasses = subpasses; + const IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + { + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .srcAccessMask = asset::ACCESS_FLAGS::NONE, + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + }, + { + .srcSubpass = 0, + .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .memoryBarrier = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .srcAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .dstAccessMask = asset::ACCESS_FLAGS::NONE + } + }, + IGPURenderpass::SCreationParams::DependenciesEnd + }; + params.dependencies = dependencies; + m_renderpass = m_device->createRenderpass(std::move(params)); + if (!m_renderpass) + return logFail("Failed to Create a Renderpass!"); + } + + ISwapchain::SSharedCreationParams sharedParams = {}; + sharedParams.imageUsage |= IGPUImage::EUF_TRANSFER_SRC_BIT; + if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::make_unique(), sharedParams)) + return logFail("Failed to Create a Swapchain!"); + + const auto presentationExtent = getPresentationRenderExtent(); + for (uint32_t i = 0u; i < MaxFramesInFlight; i++) + { + auto& image = m_tripleBuffers[i]; + { + IGPUImage::SCreationParams params = {}; + params = asset::IImage::SCreationParams{ + .type = IGPUImage::ET_2D, + .samples = samples, + .format = format, + .extent = { presentationExtent.x,presentationExtent.y,1 }, + .mipLevels = 1, + .arrayLayers = 1, + .flags = IGPUImage::ECF_NONE, + .usage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_TRANSFER_SRC_BIT + }; + image = m_device->createImage(std::move(params)); + if (!image) + return logFail("Failed to Create Triple Buffer Image!"); + + if (!m_device->allocate(image->getMemoryReqs(), image.get()).isValid()) + return logFail("Failed to allocate Device Memory for Image %d", i); + } + image->setObjectDebugName(("Triple Buffer Image " + std::to_string(i)).c_str()); + + auto imageView = m_device->createImageView({ + .flags = IGPUImageView::ECF_NONE, + .subUsages = IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_TRANSFER_SRC_BIT, + .image = core::smart_refctd_ptr(image), + .viewType = IGPUImageView::ET_2D, + .format = format + }); + const auto& imageParams = image->getCreationParameters(); + IGPUFramebuffer::SCreationParams params = { { + .renderpass = core::smart_refctd_ptr(m_renderpass), + .depthStencilAttachments = nullptr, + .colorAttachments = &imageView.get(), + .width = imageParams.extent.width, + .height = imageParams.extent.height, + .layers = imageParams.arrayLayers + } }; + m_framebuffers[i] = m_device->createFramebuffer(std::move(params)); + if (!m_framebuffers[i]) + return logFail("Failed to Create a Framebuffer for Image %d", i); + } + + auto pool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + if (!pool || !pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data(),MaxFramesInFlight }, core::smart_refctd_ptr(m_logger))) + return logFail("Failed to Create CommandBuffers!"); + + return true; +} diff --git a/61_UI/AppPresetPlayback.cpp b/61_UI/AppPresetPlayback.cpp new file mode 100644 index 000000000..141c9044b --- /dev/null +++ b/61_UI/AppPresetPlayback.cpp @@ -0,0 +1,226 @@ +#include "app/App.hpp" + +#include + +bool App::tryCaptureGoal(ICamera* camera, CCameraGoal& out) const +{ + const auto capture = m_cameraGoalSolver.captureDetailed(camera); + out = capture.goal; + return capture.captured; +} + +App::PresetUiAnalysis App::analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const +{ + return nbl::ui::CCameraPresentationUtilities::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); +} + +App::CaptureUiAnalysis App::analyzeCameraCaptureForUi(ICamera* camera) const +{ + return nbl::ui::CCameraPresentationUtilities::analyzeCapturePresentation(m_cameraGoalSolver, camera); +} + +CCameraGoalSolver::SCompatibilityResult App::analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const +{ + return nbl::core::CCameraGoalAnalysisUtilities::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; +} + +bool App::presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const +{ + return analyzePresetForUi(camera, preset).matchesFilter(m_presetAuthoring.filterMode); +} + +CCameraGoalSolver::SApplyResult App::applyPresetFromUi(ICamera* camera, const CameraPreset& preset) +{ + const auto result = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(m_cameraGoalSolver, camera, preset); + if (result.succeeded()) + refreshFollowOffsetConfigsForCamera(camera); + + const auto presetUi = analyzePresetForUi(camera, preset); + storeApplyStatusBanner( + m_presetAuthoring.applyBanner, + CCameraTextUtilities::describeApplyResult(result) + " | " + presetUi.compatibilityLabel, + result.succeeded(), + result.approximate()); + return result; +} + +void App::storeApplyStatusBanner(ApplyStatusBanner& banner, std::string summary, const bool succeeded, const bool approximate) +{ + banner.summary = std::move(summary); + banner.succeeded = succeeded; + banner.approximate = approximate; +} + +void App::clearApplyStatusBanner(ApplyStatusBanner& banner) +{ + banner.summary.clear(); + banner.succeeded = false; + banner.approximate = false; +} + +void App::storePlaybackApplySummary(const SCameraPresetApplySummary& summary) +{ + const auto& playbackAuthoring = m_playbackAuthoring; + storeApplyStatusBanner( + m_playbackAuthoring.applyBanner, + nbl::ui::CCameraTextUtilities::describePresetApplySummary( + summary, + playbackAuthoring.affectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), + summary.succeeded(), + summary.approximate()); +} + +void App::appendVirtualEventLog( + std::string_view source, + std::string_view inputSource, + const uint32_t planarIx, + ICamera* camera, + const CVirtualGimbalEvent* events, + const uint32_t count) +{ + m_uiMetrics.virtualEventsThisFrame += count; + const std::string sourceStr(source); + const std::string inputSourceStr(inputSource); + const std::string cameraName = camera ? std::string(camera->getIdentifier()) : std::string("None"); + for (uint32_t i = 0u; i < count; ++i) + { + const auto* eventName = CVirtualGimbalEvent::virtualEventToString(events[i].type).data(); + auto line = m_logFormatter->format( + ILogger::ELL_INFO, + "virtual frame=%llu src=%s input=%s cam=%s planar=%u event=%s mag=%.6f", + static_cast(m_realFrameIx), + sourceStr.c_str(), + inputSourceStr.c_str(), + cameraName.c_str(), + planarIx, + eventName, + events[i].magnitude); + m_eventLog.entries.push_back({ + m_realFrameIx, + events[i].type, + events[i].magnitude, + sourceStr, + inputSourceStr, + cameraName, + planarIx, + std::move(line) + }); + } + + while (m_eventLog.entries.size() > SCameraAppRuntimeDefaults::VirtualEventLogMax) + m_eventLog.entries.pop_front(); +} + +SCameraPresetApplySummary App::applyPresetToTargets(const CameraPreset& preset) +{ + const auto& playbackAuthoring = m_playbackAuthoring; + SCameraPresetApplySummary summary = {}; + if (!playbackAuthoring.affectsAll) + { + ICamera* activeCamera = getActiveCamera(); + summary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( + m_cameraGoalSolver, + std::span(&activeCamera, activeCamera ? 1u : 0u), + preset); + if (summary.succeeded()) + refreshFollowOffsetConfigsForCamera(activeCamera); + return summary; + } + + std::vector cameras; + cameras.reserve(m_viewports.windowBindings.size()); + std::unordered_set visited; + for (auto& binding : m_viewports.windowBindings) + { + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + continue; + + auto* camera = planar->getCamera(); + if (!camera) + continue; + + if (visited.insert(camera).second) + cameras.push_back(camera); + } + + summary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( + m_cameraGoalSolver, + std::span(cameras.data(), cameras.size()), + preset); + if (summary.succeeded()) + refreshAllFollowOffsetConfigs(); + return summary; +} + +bool App::tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) +{ + return nbl::core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(m_playbackAuthoring.keyframeTrack, time, preset); +} + +bool App::applyPlaybackAtTime(const float time) +{ + CameraPreset preset; + if (!tryBuildPlaybackPresetAtTime(time, preset)) + { + clearApplyStatusBanner(m_playbackAuthoring.applyBanner); + return false; + } + + storePlaybackApplySummary(applyPresetToTargets(preset)); + return true; +} + +void App::sortKeyframesByTime() +{ + nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(m_playbackAuthoring.keyframeTrack); +} + +void App::clampPlaybackTimeToKeyframes() +{ + nbl::core::CCameraPlaybackTimelineUtilities::clampPlaybackCursorToTrack(m_playbackAuthoring.keyframeTrack, m_playbackAuthoring.playback); +} + +int App::selectKeyframeNearestTime(const float time) +{ + return nbl::core::CCameraKeyframeTrackUtilities::selectKeyframeTrackNearestTime(m_playbackAuthoring.keyframeTrack, time); +} + +void App::normalizeSelectedKeyframe() +{ + nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(m_playbackAuthoring.keyframeTrack); +} + +App::CameraKeyframe* App::getSelectedKeyframe() +{ + return nbl::core::CCameraKeyframeTrackUtilities::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); +} + +const App::CameraKeyframe* App::getSelectedKeyframe() const +{ + return nbl::core::CCameraKeyframeTrackUtilities::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); +} + +bool App::replaceSelectedKeyframeFromCamera(ICamera* camera) +{ + auto* selected = getSelectedKeyframe(); + if (!selected) + return false; + + CameraPreset updatedPreset; + const auto keyframeName = selected->preset.name.empty() ? std::string("Keyframe") : selected->preset.name; + if (!nbl::core::CCameraPresetFlowUtilities::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) + return false; + + return nbl::core::CCameraKeyframeTrackUtilities::replaceSelectedKeyframePreset(m_playbackAuthoring.keyframeTrack, std::move(updatedPreset)); +} + +void App::updatePlayback(const double dtSec) +{ + const auto advance = nbl::core::CCameraPlaybackTimelineUtilities::advancePlaybackCursor(m_playbackAuthoring.playback, m_playbackAuthoring.keyframeTrack, dtSec); + if (!advance.hasTrack || !advance.changedTime) + return; + + applyPlaybackAtTime(m_playbackAuthoring.playback.time); +} + diff --git a/61_UI/AppResourceBootstrap.cpp b/61_UI/AppResourceBootstrap.cpp new file mode 100644 index 000000000..0d9cf92a7 --- /dev/null +++ b/61_UI/AppResourceBootstrap.cpp @@ -0,0 +1,11 @@ +#include "app/App.hpp" +#include "app/AppResourceUtilities.hpp" + +bool App::initializeMountedCameraResources(smart_refctd_ptr&& system) +{ + if (!asset_base_t::onAppInitialized(std::move(system))) + return false; + + nbl::system::mountOptionalSharedEnvmapResources(getCameraAppResourceContext(), m_logger.get()); + return true; +} diff --git a/61_UI/AppResourceUtilities.cpp b/61_UI/AppResourceUtilities.cpp new file mode 100644 index 000000000..99892f507 --- /dev/null +++ b/61_UI/AppResourceUtilities.cpp @@ -0,0 +1,112 @@ +#include "app/AppResourceUtilities.hpp" + +#include +#include +#include + +#include "app/AppResourcePathUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" + +inline bool parseSpaceEnvBlobBytes( + std::span blobBytes, + nbl::system::SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload) +{ + if (blobBytes.size() < sizeof(nbl::system::SSpaceEnvBlobHeader)) + return false; + + std::memcpy(&outHeader, blobBytes.data(), sizeof(outHeader)); + + if (outHeader.magic != nbl::system::SCameraEnvmapResourcePaths::SpaceEnvBlobMagic || + outHeader.format != nbl::system::SCameraEnvmapResourcePaths::SpaceEnvBlobFormatRgba16Sfloat) + { + return false; + } + if (outHeader.width == 0u || outHeader.height == 0u) + return false; + if (outHeader.payloadSize != static_cast(outHeader.width) * outHeader.height * 8ull) + return false; + if (outHeader.payloadSize > static_cast(std::numeric_limits::max())) + return false; + + const size_t payloadOffset = sizeof(outHeader); + if (blobBytes.size() != payloadOffset + static_cast(outHeader.payloadSize)) + return false; + + outPayload.resize(static_cast(outHeader.payloadSize)); + std::memcpy(outPayload.data(), blobBytes.data() + payloadOffset, outPayload.size()); + return true; +} + +inline bool loadSpaceEnvBlob( + nbl::system::ISystem& system, + const nbl::system::path& blobPath, + nbl::system::SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload) +{ + std::vector blobBytes; + if (!nbl::system::CCameraFileUtilities::readBinaryFile(system, blobPath, blobBytes)) + return false; + return parseSpaceEnvBlobBytes(blobBytes, outHeader, outPayload); +} + +namespace nbl::system +{ + +bool mountOptionalSharedEnvmapResources( + const SCameraAppResourceContext& context, + ILogger* logger) +{ + if (!context) + return false; + + auto sharedEnvmapDirectory = getSharedEnvmapDirectory(); + std::error_code ec; + if (!std::filesystem::exists(sharedEnvmapDirectory, ec) || ec) + return false; + + auto sharedEnvmapArchive = make_smart_refctd_ptr( + std::move(sharedEnvmapDirectory), + core::smart_refctd_ptr(logger), + context.system); + context.system->mount( + std::move(sharedEnvmapArchive), + SCameraMountedResourcePaths::MountedSharedEnvmapWorkingDirectory.data()); + return true; +} + +bool loadPreferredSpaceEnvBlob( + const SCameraAppResourceContext& context, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload, + path* outLoadedPath) +{ + if (!context) + return false; + + const auto candidates = makeSpaceEnvBlobCandidates(); + return loadFirstCandidatePath( + candidates.asSpan(), + [&](const path& candidate) -> bool + { + return loadSpaceEnvBlob(*context.system, candidate, outHeader, outPayload); + }, + outLoadedPath); +} + +core::smart_refctd_ptr loadPrecompiledShaderFromAppResources( + asset::IAssetManager& assetManager, + ILogger* logger, + const std::string_view key) +{ + asset::IAssetLoader::SAssetLoadParams loadParams = {}; + loadParams.logger = logger; + loadParams.workingDirectory = SCameraMountedResourcePaths::AppResourcesWorkingDirectory; + auto bundle = assetManager.getAsset(key.data(), loadParams); + const auto& contents = bundle.getContents(); + if (contents.empty()) + return nullptr; + return asset::IAsset::castDown(contents[0]); +} + +} // namespace nbl::system diff --git a/61_UI/AppSceneDebugInstances.cpp b/61_UI/AppSceneDebugInstances.cpp new file mode 100644 index 000000000..719832109 --- /dev/null +++ b/61_UI/AppSceneDebugInstances.cpp @@ -0,0 +1,59 @@ +#include "app/App.hpp" + +void App::updateAuxSceneInstances(const size_t geometryCount) +{ + const uint32_t gridInstanceIx = SCameraAppSceneDefaults::CameraObjectIxOffset - 1u; + if (m_debugScene.gridGeometryIx.has_value() && m_debugScene.renderer->m_instances.size() > gridInstanceIx) + { + const auto gridGeometryIx = m_debugScene.gridGeometryIx.value(); + if (gridGeometryIx < geometryCount) + { + auto& gridInstance = m_debugScene.renderer->m_instances[gridInstanceIx]; + gridInstance.packedGeo = m_debugScene.renderer->getGeometries().data() + gridGeometryIx; + + float32_t3x4 gridWorld = float32_t3x4(1.0f); + gridWorld[0][0] = SCameraAppSceneDebugDefaults::GridExtent; + gridWorld[2][2] = SCameraAppSceneDebugDefaults::GridExtent; + gridWorld[0][3] = -0.5f * SCameraAppSceneDebugDefaults::GridExtent; + gridWorld[1][3] = SCameraAppSceneDebugDefaults::GridVerticalOffset; + gridWorld[2][3] = -0.5f * SCameraAppSceneDebugDefaults::GridExtent; + gridInstance.world = gridWorld; + } + } + + const uint32_t followInstanceIx = m_debugScene.gridGeometryIx.has_value() ? + SCameraAppSceneDefaults::CameraObjectIxOffset : + SCameraAppSceneDefaults::FollowTargetObjectIx; + if (m_debugScene.renderer->m_instances.size() <= followInstanceIx) + return; + + auto& followInstance = m_debugScene.renderer->m_instances[followInstanceIx]; + if (m_sceneInteraction.followTargetVisible && m_debugScene.followTargetGeometryIx.has_value() && m_debugScene.followTargetGeometryIx.value() < geometryCount) + { + followInstance.packedGeo = m_debugScene.renderer->getGeometries().data() + m_debugScene.followTargetGeometryIx.value(); + followInstance.world = computeFollowTargetMarkerWorld(); + } + else + { + followInstance.packedGeo = nullptr; + followInstance.world = float32_t3x4(1.0f); + } +} + +void App::updateSceneDebugInstances() +{ + if (!m_debugScene.renderer || m_debugScene.renderer->m_instances.empty()) + return; + + auto& modelInstance = m_debugScene.renderer->m_instances[SCameraAppSceneDefaults::ModelObjectIx]; + modelInstance.world = m_sceneInteraction.model; + + const auto geometryCount = m_debugScene.renderer->getGeometries().size(); + if (geometryCount) + { + if (m_debugScene.geometrySelectionIx >= geometryCount) + m_debugScene.geometrySelectionIx = 0u; + modelInstance.packedGeo = m_debugScene.renderer->getGeometries().data() + m_debugScene.geometrySelectionIx; + } + updateAuxSceneInstances(geometryCount); +} diff --git a/61_UI/AppSceneFramebufferResources.cpp b/61_UI/AppSceneFramebufferResources.cpp new file mode 100644 index 000000000..8ec5b86b3 --- /dev/null +++ b/61_UI/AppSceneFramebufferResources.cpp @@ -0,0 +1,142 @@ +#include "app/App.hpp" + +smart_refctd_ptr createSceneAttachmentView(ILogicalDevice* device, E_FORMAT format, uint32_t width, uint32_t height, const char* debugName) +{ + if (!device) + return nullptr; + + const bool isDepth = isDepthOrStencilFormat(format); + auto usage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT; + if (!isDepth) + usage |= IGPUImage::EUF_SAMPLED_BIT; + + auto image = device->createImage({{ + .type = IGPUImage::ET_2D, + .samples = IGPUImage::ESCF_1_BIT, + .format = format, + .extent = { width, height, 1u }, + .mipLevels = 1u, + .arrayLayers = 1u, + .usage = usage + }}); + if (!image) + return nullptr; + + image->setObjectDebugName(debugName); + if (!device->allocate(image->getMemoryReqs(), image.get()).isValid()) + return nullptr; + + IGPUImageView::SCreationParams params = { + .subUsages = usage, + .image = std::move(image), + .viewType = IGPUImageView::ET_2D, + .format = format + }; + params.subresourceRange.aspectMask = isDepth ? IGPUImage::EAF_DEPTH_BIT : IGPUImage::EAF_COLOR_BIT; + return device->createImageView(std::move(params)); +} + +smart_refctd_ptr createSceneFramebuffer( + ILogicalDevice* device, + IGPURenderpass* renderpass, + IGPUImageView* colorView, + IGPUImageView* depthView) +{ + if (!device || !renderpass || !colorView || !depthView) + return nullptr; + + const auto& imageParams = colorView->getCreationParameters().image->getCreationParameters(); + IGPUFramebuffer::SCreationParams params = { { + .renderpass = core::smart_refctd_ptr(renderpass), + .depthStencilAttachments = &depthView, + .colorAttachments = &colorView, + .width = imageParams.extent.width, + .height = imageParams.extent.height, + .layers = imageParams.arrayLayers + } }; + return device->createFramebuffer(std::move(params)); +} + +bool App::initializeSceneRenderpass() +{ + IGPURenderpass::SCreationParams params = {}; + const IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { + {{ + { + .format = SCameraAppRenderDefaults::SceneDepthFormat, + .samples = IGPUImage::ESCF_1_BIT, + .mayAlias = false + }, + { IGPURenderpass::LOAD_OP::CLEAR }, + { IGPURenderpass::STORE_OP::STORE }, + { IGPUImage::LAYOUT::UNDEFINED }, + { IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL } + }}, + IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd + }; + params.depthStencilAttachments = depthAttachments; + const IGPURenderpass::SCreationParams::SColorAttachmentDescription colorAttachments[] = { + {{ + { + .format = SCameraAppRenderDefaults::FinalSceneFormat, + .samples = IGPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT, + .mayAlias = false + }, + IGPURenderpass::LOAD_OP::CLEAR, + IGPURenderpass::STORE_OP::STORE, + IGPUImage::LAYOUT::UNDEFINED, + IGPUImage::LAYOUT::READ_ONLY_OPTIMAL + }}, + IGPURenderpass::SCreationParams::ColorAttachmentsEnd + }; + params.colorAttachments = colorAttachments; + IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { + {}, + IGPURenderpass::SCreationParams::SubpassesEnd + }; + subpasses[0].depthStencilAttachment = { { .render = { .attachmentIndex = 0, .layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL } } }; + subpasses[0].colorAttachments[0] = { .render = { .attachmentIndex = 0, .layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL } }; + params.subpasses = subpasses; + static constexpr IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + { + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT | PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, + .srcAccessMask = ACCESS_FLAGS::NONE, + .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT | PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + }, + { + .srcSubpass = 0, + .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .memoryBarrier = { + .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, + .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT | PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT, + .dstAccessMask = ACCESS_FLAGS::SAMPLED_READ_BIT + } + }, + IGPURenderpass::SCreationParams::DependenciesEnd + }; + params.dependencies = dependencies; + m_debugScene.renderpass = m_device->createRenderpass(std::move(params)); + return m_debugScene.renderpass || logFail("Failed to create Scene Renderpass!"); +} + +bool App::initializeWindowSceneFramebufferResources() +{ + const auto presentationExtent = getPresentationRenderExtent(); + for (uint32_t i = 0u; i < m_viewports.windowBindings.size(); ++i) + { + auto& binding = m_viewports.windowBindings[i]; + binding.sceneColorView = createSceneAttachmentView(m_device.get(), SCameraAppRenderDefaults::FinalSceneFormat, presentationExtent.x, presentationExtent.y, "UI Scene Color Attachment"); + binding.sceneDepthView = createSceneAttachmentView(m_device.get(), SCameraAppRenderDefaults::SceneDepthFormat, presentationExtent.x, presentationExtent.y, "UI Scene Depth Attachment"); + binding.sceneFramebuffer = createSceneFramebuffer(m_device.get(), m_debugScene.renderpass.get(), binding.sceneColorView.get(), binding.sceneDepthView.get()); + if (!binding.sceneFramebuffer) + return logFail("Could not create geometry creator scene[%d]!", i); + } + + return true; +} diff --git a/61_UI/AppSceneRenderPasses.cpp b/61_UI/AppSceneRenderPasses.cpp new file mode 100644 index 000000000..bc502a065 --- /dev/null +++ b/61_UI/AppSceneRenderPasses.cpp @@ -0,0 +1,80 @@ +#include "app/App.hpp" +#include "app/AppRenderPassUtilities.hpp" + +bool App::recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBinding& binding, const uint32_t) +{ + if (!cmdbuf || !binding.sceneFramebuffer) + return true; + + const auto& framebufferParams = binding.sceneFramebuffer->getCreationParameters(); + const auto renderArea = makeRenderArea(framebufferParams.width, framebufferParams.height); + const IGPUCommandBuffer::SRenderpassBeginInfo renderPassInfo = { + .framebuffer = binding.sceneFramebuffer.get(), + .colorClearValues = &SCameraAppRenderDefaults::SceneClearColor, + .depthStencilClearValues = &SCameraAppRenderDefaults::SceneClearDepth, + .renderArea = renderArea + }; + + bool success = cmdbuf->beginRenderPass(renderPassInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + const auto finalize = [&]() -> bool + { + success = success && cmdbuf->endRenderPass(); + return success; + }; + + const auto viewport = makeFramebufferViewport(framebufferParams.width, framebufferParams.height); + success = success && cmdbuf->setViewport(0u, 1u, &viewport); + success = success && cmdbuf->setScissor(0u, 1u, &renderArea); + + if (m_spaceEnvironment.pipeline && m_spaceEnvironment.descriptorSet) + { + auto* pipelineLayout = m_spaceEnvironment.pipeline->getLayout(); + const IGPUDescriptorSet* descriptorSets[] = { m_spaceEnvironment.descriptorSet.get() }; + SpaceEnvPushConstants pushConstants = {}; + pushConstants.invProj = hlsl::inverse(binding.projectionMatrix); + pushConstants.invViewRot = buildInverseViewRotation(binding.viewMatrix); + pushConstants.orthoMode = binding.isOrthographicProjection ? 1u : 0u; + + success = success && cmdbuf->bindGraphicsPipeline(m_spaceEnvironment.pipeline.get()); + success = success && cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipelineLayout, 0u, 1u, descriptorSets); + success = success && cmdbuf->pushConstants(pipelineLayout, IShader::E_SHADER_STAGE::ESS_FRAGMENT, 0u, sizeof(pushConstants), &pushConstants); + success = success && nbl::ext::FullScreenTriangle::recordDrawCall(cmdbuf); + } + + const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); + m_debugScene.renderer->render(cmdbuf, viewParams); + return finalize(); +} + +bool App::recordUiRenderPass(IGPUCommandBuffer* cmdbuf, const uint32_t resourceIx) +{ + if (!cmdbuf) + return false; + + const auto uiClearColor = SCameraAppFrameRuntimeDefaults::UiClearColor; + const auto renderArea = makeRenderArea(m_window->getWidth(), m_window->getHeight()); + const IGPUCommandBuffer::SRenderpassBeginInfo renderPassInfo = { + .framebuffer = m_framebuffers[resourceIx].get(), + .colorClearValues = &uiClearColor, + .depthStencilClearValues = nullptr, + .renderArea = renderArea + }; + + bool success = cmdbuf->beginRenderPass(renderPassInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + const auto viewport = makeFramebufferViewport(m_window->getWidth(), m_window->getHeight()); + success = success && cmdbuf->setViewport(0u, 1u, &viewport); + + auto* pipeline = m_ui.manager->getPipeline(); + const auto uiParams = m_ui.manager->getCreationParameters(); + const nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; + + success = success && cmdbuf->bindGraphicsPipeline(pipeline); + success = success && cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); + + if (!keepRunning()) + return false; + + success = success && m_ui.manager->render(cmdbuf, waitInfo); + success = success && cmdbuf->endRenderPass(); + return success; +} diff --git a/61_UI/AppSceneResources.cpp b/61_UI/AppSceneResources.cpp new file mode 100644 index 000000000..bfa53908a --- /dev/null +++ b/61_UI/AppSceneResources.cpp @@ -0,0 +1,17 @@ +#include "app/App.hpp" + +bool App::initializeSceneResources() +{ + if (!initializeGeometrySceneResources()) + return false; + if (!initializeSceneRenderpass()) + return false; + if (!initializeSpaceEnvironmentResources()) + return false; + if (!initializeDebugSceneRendererResources()) + return false; + if (!initializeWindowSceneFramebufferResources()) + return false; + + return true; +} diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp new file mode 100644 index 000000000..74fbe8925 --- /dev/null +++ b/61_UI/AppScriptedInitialization.cpp @@ -0,0 +1,241 @@ +#include "app/App.hpp" + +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" + +void App::resetScriptedInputRuntimeState() +{ + m_scriptedInput.nextEventIndex = 0u; + m_scriptedInput.nextActionIndex = 0u; + m_scriptedInput.checkRuntime = {}; + m_scriptedInput.nextCaptureIndex = 0u; + m_scriptedInput.failed = false; + m_scriptedInput.summaryReported = false; +} + +void App::finalizeScriptedInputRuntimeState() +{ + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(m_scriptedInput.timeline, m_cliRuntime.disableScreenshotsCli); +} + +void App::applyParsedScriptedInput( + nbl::this_example::CCameraScriptedInputParseResult parsed, + std::optional& pendingScriptedSequence) +{ + pendingScriptedSequence.reset(); + m_scriptedInput.timeline.clear(); + m_scriptedInput.actionEvents.clear(); + resetScriptedInputRuntimeState(); + m_scriptedInput.exclusive = false; + m_scriptedInput.hardFail = false; + m_scriptedInput.visualDebug = false; + m_scriptedInput.visualTargetFps = 0.f; + m_scriptedInput.visualCameraHoldSeconds = 0.f; + m_scriptedInput.visualPlanar = {}; + m_scriptedInput.visualFollow = {}; + m_scriptedInput.scriptedMouseButtons = {}; + m_scriptedInput.framePacer = {}; + m_scriptedInput.capturePrefix = std::string(SCameraAppScriptedVisualDefaults::DefaultCapturePrefix); + m_scriptedInput.captureOutputDir = localOutputCWD; + + m_scriptedInput.enabled = parsed.enabled; + if (parsed.hasLog) + m_scriptedInput.log = parsed.log || m_scriptedInput.log; + m_scriptedInput.hardFail = parsed.hardFail; + m_scriptedInput.visualDebug = parsed.visualDebug; + m_scriptedInput.visualTargetFps = parsed.visualTargetFps; + m_scriptedInput.visualCameraHoldSeconds = parsed.visualCameraHoldSeconds; + if (m_cliRuntime.scriptVisualDebugCli) + m_scriptedInput.visualDebug = true; + if (m_scriptedInput.visualDebug) + { + if (m_scriptedInput.visualTargetFps <= 0.f) + m_scriptedInput.visualTargetFps = SCameraAppScriptedVisualDefaults::TargetFps; + if (m_scriptedInput.visualCameraHoldSeconds <= 0.f) + m_scriptedInput.visualCameraHoldSeconds = SCameraAppScriptedVisualDefaults::HoldSeconds; + } + + if (parsed.hasEnableActiveCameraMovement) + m_viewports.enableActiveCameraMovement = parsed.enableActiveCameraMovement; + else if (m_scriptedInput.enabled) + m_viewports.enableActiveCameraMovement = true; + + m_scriptedInput.exclusive = parsed.exclusive; + m_scriptedInput.capturePrefix = parsed.capturePrefix.empty() ? std::string(SCameraAppScriptedVisualDefaults::DefaultCapturePrefix) : parsed.capturePrefix; + + if (parsed.cameraControls.hasKeyboardScale) + m_cameraControls.keyboardScale = parsed.cameraControls.keyboardScale; + if (parsed.cameraControls.hasMouseMoveScale) + m_cameraControls.mouseMoveScale = parsed.cameraControls.mouseMoveScale; + if (parsed.cameraControls.hasMouseScrollScale) + m_cameraControls.mouseScrollScale = parsed.cameraControls.mouseScrollScale; + if (parsed.cameraControls.hasTranslationScale) + m_cameraControls.translationScale = parsed.cameraControls.translationScale; + if (parsed.cameraControls.hasRotationScale) + m_cameraControls.rotationScale = parsed.cameraControls.rotationScale; + + for (const auto& warning : parsed.warnings) + m_logger->log("%s", ILogger::ELL_WARNING, warning.c_str()); + + pendingScriptedSequence = std::move(parsed.sequence); + m_scriptedInput.timeline = std::move(parsed.timeline); + m_scriptedInput.actionEvents = std::move(parsed.actionEvents); + finalizeScriptedInputRuntimeState(); +} + +bool App::tryLoadConfiguredScriptedInput( + const argparse::ArgumentParser& program, + const nbl::system::SCameraConfigCollections& cameraCollections, + std::optional& outPendingScriptedSequence) +{ + outPendingScriptedSequence = std::nullopt; + + const auto tryApplyScriptedText = [&](const std::string_view scriptedText) -> bool + { + if (scriptedText.empty()) + return true; + + nbl::this_example::CCameraScriptedInputParseResult parsed = {}; + std::string scriptedInputParseError; + if (!nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::readCameraScriptedInput(scriptedText, parsed, &scriptedInputParseError)) + return logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); + + applyParsedScriptedInput(std::move(parsed), outPendingScriptedSequence); + return true; + }; + + if (program.is_used("--script")) + { + nbl::system::SCameraScriptTextLoadResult scriptResource = {}; + std::string scriptedInputLoadError; + if (!nbl::system::tryLoadCameraScriptText( + getCameraAppResourceContext(), + nbl::system::path(program.get("--script")), + scriptResource, + &scriptedInputLoadError)) + { + return logFail("Camera sequence script parse failed: %s", scriptedInputLoadError.c_str()); + } + + return tryApplyScriptedText(scriptResource.text); + } + + std::string embeddedScriptedInput = {}; + if (!nbl::system::tryGetEmbeddedCameraScriptedInputText(cameraCollections, embeddedScriptedInput)) + return true; + + return tryApplyScriptedText(embeddedScriptedInput); +} + +std::optional App::resolveSequenceSegmentPlanarIx(const CCameraSequenceSegment& segment) const +{ + std::optional match; + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) + { + auto* camera = m_planarProjections[planarIx]->getCamera(); + if (!camera) + continue; + + const bool kindMatch = segment.cameraKind == ICamera::CameraKind::Unknown || camera->getKind() == segment.cameraKind; + const bool identifierMatch = segment.cameraIdentifier.empty() || camera->getIdentifier() == segment.cameraIdentifier; + if (!(kindMatch && identifierMatch)) + continue; + + if (match.has_value()) + return std::nullopt; + match = planarIx; + } + + return match; +} + +bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) +{ + CCameraScriptedTimeline timeline; + std::vector actionEvents; + resetScriptedInputRuntimeState(); + + const bool useWindowMode = nbl::core::CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(sequence); + nbl::this_example::CCameraScriptedActionUtilities::appendActionEvent( + actionEvents, + 0u, + nbl::this_example::ECameraScriptedActionCode::SetUseWindow, + useWindowMode ? 1 : 0); + + CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = {}; + referenceTrackedTargetPose.position = getDefaultFollowTargetPosition(); + referenceTrackedTargetPose.orientation = getDefaultFollowTargetOrientation(); + + uint64_t frameCursor = 0u; + for (const auto& segment : sequence.segments) + { + const auto planarIx = resolveSequenceSegmentPlanarIx(segment); + if (!planarIx.has_value()) + { + const auto kindLabel = segment.cameraKind != ICamera::CameraKind::Unknown ? std::string(CCameraTextUtilities::getCameraTypeLabel(segment.cameraKind)) : std::string("Unknown"); + return logFail( + "Sequence segment \"%s\" has ambiguous or missing camera match for kind \"%s\" identifier \"%s\".", + segment.name.c_str(), + kindLabel.c_str(), + segment.cameraIdentifier.c_str()); + } + + const bool useTrackedTargetFollow = + nbl::core::CCameraSequenceScriptUtilities::sequenceSegmentUsesTrackedTargetTrack(segment) && + planarIx.value() < m_sceneInteraction.planarFollowConfigs.size() && + m_sceneInteraction.planarFollowConfigs[planarIx.value()].enabled && + m_sceneInteraction.planarFollowConfigs[planarIx.value()].mode != ECameraFollowMode::Disabled; + + nbl::core::CCameraSequenceCompiledSegment compiledSegment; + std::string trackError; + if (!nbl::core::CCameraSequenceScriptUtilities::compileSequenceSegmentFromReference( + sequence, + segment, + m_presetAuthoring.initialPlanarPresets[planarIx.value()], + referenceTrackedTargetPose, + compiledSegment, + &trackError)) + { + return logFail("Sequence segment \"%s\" failed to compile: %s", segment.name.c_str(), trackError.c_str()); + } + + if (compiledSegment.presentations.size() > m_viewports.windowBindings.size()) + { + m_logger->log( + "Sequence segment \"%s\" requests %zu presentations, only %zu windows are available. Extra presentations will be ignored.", + ILogger::ELL_WARNING, + segment.name.c_str(), + compiledSegment.presentations.size(), + m_viewports.windowBindings.size()); + } + + std::string buildError; + if (!nbl::this_example::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( + timeline, + actionEvents, + frameCursor, + compiledSegment, + { + .planarIx = planarIx.value(), + .availableWindowCount = m_viewports.windowBindings.size(), + .useWindow = useWindowMode, + .includeFollowTargetLock = useTrackedTargetFollow + }, + &buildError)) + { + return logFail( + "Sequence segment \"%s\" failed to build scripted runtime data: %s", + segment.name.c_str(), + buildError.c_str()); + } + + frameCursor += compiledSegment.durationFrames; + } + + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline, m_cliRuntime.disableScreenshotsCli); + nbl::this_example::CCameraScriptedActionUtilities::finalizeActionEvents(actionEvents); + m_scriptedInput.timeline = std::move(timeline); + m_scriptedInput.actionEvents = std::move(actionEvents); + return true; +} + diff --git a/61_UI/AppScriptedInputRuntime.cpp b/61_UI/AppScriptedInputRuntime.cpp new file mode 100644 index 000000000..8c749a395 --- /dev/null +++ b/61_UI/AppScriptedInputRuntime.cpp @@ -0,0 +1,320 @@ +#include "app/App.hpp" + +void App::logScriptedCameraPose(const char* label, ICamera* camera) const +{ + if (!(m_scriptedInput.log && camera)) + return; + + const auto& gimbal = camera->getGimbal(); + const auto position = gimbal.getPosition(); + const auto euler = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + m_logger->log( + "[script] %s gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", + ILogger::ELL_INFO, + label, + position.x, + position.y, + position.z, + euler.x, + euler.y, + euler.z); +} + +void App::dequeueScriptedFrameInput(SScriptedFrameInputState& outFrame) +{ + outFrame = {}; + + if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) + { + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents( + m_scriptedInput.timeline.events, + m_scriptedInput.nextEventIndex, + m_realFrameIx, + outFrame.frameEvents); + } + if (m_scriptedInput.enabled && m_scriptedInput.nextActionIndex < m_scriptedInput.actionEvents.size()) + { + nbl::this_example::CCameraScriptedActionUtilities::dequeueFrameActions( + m_scriptedInput.actionEvents, + m_scriptedInput.nextActionIndex, + m_realFrameIx, + outFrame.actions); + } + + nbl::ui::CCameraScriptedUiInputUtilities::appendScriptedUiInputEvents( + m_nextPresentationTimestamp, + m_window.get(), + outFrame.frameEvents.keyboard, + outFrame.frameEvents.mouse, + outFrame.keyboard, + outFrame.mouse); + + if (!outFrame.frameEvents.segmentLabels.empty()) + m_scriptedInput.visualPlanar.segmentLabel = outFrame.frameEvents.segmentLabels.back(); +} + +void App::applyScriptedFrameActions(std::span scriptedActions) +{ + if (!(m_scriptedInput.enabled && !scriptedActions.empty())) + return; + + auto applyAction = [&](const nbl::this_example::CCameraScriptedActionEvent& action) -> void + { + switch (static_cast(action.code)) + { + case nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow: + { + if (action.value < 0 || static_cast(action.value) >= m_viewports.windowBindings.size()) + { + m_logger->log("[script][warn] action set_active_render_window out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + m_viewports.activeRenderWindowIx = static_cast(action.value); + } break; + + case nbl::this_example::ECameraScriptedActionCode::SetActivePlanar: + { + if (action.value < 0) + { + m_logger->log("[script][warn] action set_active_planar out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + + auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + if (!nbl::ui::trySelectBindingPlanar( + getPlanarProjectionSpan(), + binding, + static_cast(action.value))) + { + m_logger->log("[script][warn] action set_active_planar out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + m_scriptedInput.visualPlanar.valid = true; + m_scriptedInput.visualPlanar.planarIx = binding.activePlanarIx; + m_scriptedInput.visualPlanar.startFrame = m_realFrameIx; + } break; + + case nbl::this_example::ECameraScriptedActionCode::SetProjectionType: + { + auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + const auto type = static_cast(action.value); + if (!nbl::ui::trySelectBindingProjectionType( + getPlanarProjectionSpan(), + binding, + type)) + { + m_logger->log("[script][warn] action set_projection_type invalid value: %d", ILogger::ELL_WARNING, action.value); + } + } break; + + case nbl::this_example::ECameraScriptedActionCode::SetProjectionIndex: + { + auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + auto& projections = m_planarProjections[binding.activePlanarIx]->getPlanarProjections(); + if (action.value < 0 || static_cast(action.value) >= projections.size()) + { + m_logger->log("[script][warn] action set_projection_index out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + + nbl::ui::trySelectBindingProjectionIndex( + getPlanarProjectionSpan(), + binding, + static_cast(action.value)); + } break; + + case nbl::this_example::ECameraScriptedActionCode::SetUseWindow: + m_viewports.useWindow = action.value != 0; + break; + + case nbl::this_example::ECameraScriptedActionCode::SetLeftHanded: + m_viewports.windowBindings[m_viewports.activeRenderWindowIx].leftHandedProjection = action.value != 0; + break; + + case nbl::this_example::ECameraScriptedActionCode::ResetActiveCamera: + { + auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + if (binding.activePlanarIx >= m_planarProjections.size()) + { + m_logger->log("[script][warn] action reset_active_camera active planar out of range: %u", ILogger::ELL_WARNING, binding.activePlanarIx); + return; + } + if (binding.activePlanarIx >= m_presetAuthoring.initialPlanarPresets.size()) + { + m_logger->log("[script][warn] action reset_active_camera missing initial preset for planar: %u", ILogger::ELL_WARNING, binding.activePlanarIx); + return; + } + + auto* camera = m_planarProjections[binding.activePlanarIx]->getCamera(); + if (!nbl::core::CCameraPresetFlowUtilities::applyPreset(m_cameraGoalSolver, camera, m_presetAuthoring.initialPlanarPresets[binding.activePlanarIx])) + m_logger->log("[script][warn] action reset_active_camera failed for planar: %u", ILogger::ELL_WARNING, binding.activePlanarIx); + } break; + } + }; + + for (const auto& action : scriptedActions) + { + if (nbl::this_example::CCameraScriptedActionUtilities::hasCode(action, nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow)) + applyAction(action); + } + + for (const auto& action : scriptedActions) + { + if (!nbl::this_example::CCameraScriptedActionUtilities::hasCode(action, nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow)) + applyAction(action); + } + + if (m_scriptedInput.log) + { + m_logger->log( + "[script] frame %llu actions=%zu", + ILogger::ELL_INFO, + static_cast(m_realFrameIx), + scriptedActions.size()); + } +} + +void App::ensureScriptedVisualPlanarState() +{ + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug && !m_scriptedInput.visualPlanar.valid)) + return; + if (m_viewports.activeRenderWindowIx >= m_viewports.windowBindings.size()) + return; + + m_scriptedInput.visualPlanar.valid = true; + m_scriptedInput.visualPlanar.planarIx = m_viewports.windowBindings[m_viewports.activeRenderWindowIx].activePlanarIx; + m_scriptedInput.visualPlanar.startFrame = m_realFrameIx; +} + +void App::updateScriptedMouseButtons(std::span scriptedMouse) +{ + if (!m_scriptedInput.enabled) + { + m_scriptedInput.scriptedMouseButtons.leftDown = false; + m_scriptedInput.scriptedMouseButtons.rightDown = false; + return; + } + + for (const auto& event : scriptedMouse) + { + if (event.type != ui::SMouseEvent::EET_CLICK) + continue; + + if (event.clickEvent.mouseButton == ui::EMB_LEFT_BUTTON) + { + if (event.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + m_scriptedInput.scriptedMouseButtons.leftDown = true; + else if (event.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + m_scriptedInput.scriptedMouseButtons.leftDown = false; + } + else if (event.clickEvent.mouseButton == ui::EMB_RIGHT_BUTTON) + { + if (event.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + m_scriptedInput.scriptedMouseButtons.rightDown = true; + else if (event.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + m_scriptedInput.scriptedMouseButtons.rightDown = false; + } + } +} + +void App::appendScriptedInputEvents(const SScriptedFrameInputState& scriptedFrame, SCapturedUiEvents& capturedEvents) +{ + updateScriptedMouseButtons(scriptedFrame.mouse); + + if (!scriptedFrame.mouse.empty()) + capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedFrame.mouse.begin(), scriptedFrame.mouse.end()); + if (!scriptedFrame.keyboard.empty()) + capturedEvents.keyboard.insert(capturedEvents.keyboard.end(), scriptedFrame.keyboard.begin(), scriptedFrame.keyboard.end()); +} + +void App::syncDynamicPerspectiveForPlanar(planar_projection_t* planar, ICamera* camera) +{ + if (!planar || !camera) + return; + + for (auto& projection : planar->getPlanarProjections()) + nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(camera, projection); +} + +void App::logScriptedVirtualEvents(const char* label, std::span events) const +{ + if (!m_scriptedInput.log) + return; + + for (const auto& event : events) + { + m_logger->log( + "[script] %s virtual %s magnitude=%.6f", + ILogger::ELL_INFO, + label, + CVirtualGimbalEvent::virtualEventToString(event.type).data(), + event.magnitude); + } +} + +void App::applyScriptedImguizmoInput(SScriptedFrameInputState& scriptedFrame, const bool skipCameraInput) +{ + scriptedFrame.imguizmoVirtualEvents.clear(); + if (!(m_scriptedInput.enabled && !scriptedFrame.frameEvents.imguizmo.empty() && !skipCameraInput)) + return; + + SActiveScriptedCameraContext runtimeContext = {}; + if (!tryBuildActiveScriptedCameraContext(runtimeContext)) + return; + auto& binding = *runtimeContext.viewport.binding; + auto* camera = runtimeContext.viewport.camera; + + CGimbalInputBinder imguizmoBinding; + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(imguizmoBinding, *camera); + auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { + .imguizmoEvents = { scriptedFrame.frameEvents.imguizmo.data(), scriptedFrame.frameEvents.imguizmo.size() } + }); + auto& imguizmoEvents = collectedEvents.events; + const uint32_t virtualEventCount = collectedEvents.imguizmoCount; + if (!virtualEventCount) + return; + + scriptedFrame.imguizmoVirtualEvents.assign(imguizmoEvents.begin(), imguizmoEvents.begin() + virtualEventCount); + const auto virtualEventSpan = std::span(scriptedFrame.imguizmoVirtualEvents.data(), virtualEventCount); + camera->manipulate(virtualEventSpan); + appendVirtualEventLog("imguizmo", "ImGuizmo", binding.activePlanarIx, camera, virtualEventSpan.data(), virtualEventCount); + logScriptedVirtualEvents("imguizmo", virtualEventSpan); + logScriptedCameraPose("imguizmo", camera); +} + +void App::applyScriptedGoals(const CCameraScriptedFrameEvents& scriptedFrameEvents, const bool skipCameraInput) +{ + if (!(m_scriptedInput.enabled && !scriptedFrameEvents.goals.empty() && !skipCameraInput)) + return; + + SActiveScriptedCameraContext runtimeContext = {}; + if (!tryBuildActiveScriptedCameraContext(runtimeContext)) + return; + auto& planar = *runtimeContext.viewport.planar; + auto* camera = runtimeContext.viewport.camera; + + auto logGoalFail = [&](const char* fmt, auto&&... args) -> void + { + m_scriptedInput.failed = true; + m_logger->log(fmt, ILogger::ELL_ERROR, std::forward(args)...); + }; + + for (const auto& goalEvent : scriptedFrameEvents.goals) + { + const auto result = m_cameraGoalSolver.applyDetailed(camera, goalEvent.goal); + if (!result.succeeded() || (goalEvent.requireExact && !result.exact)) + { + logGoalFail( + "[script][fail] goal_apply frame=%llu status=%s exact=%d details=%s", + static_cast(m_realFrameIx), + result.succeeded() ? "inexact" : "failed", + result.exact ? 1 : 0, + CCameraTextUtilities::describeApplyResult(result).c_str()); + } + } + + syncDynamicPerspectiveForPlanar(&planar, camera); + + logScriptedCameraPose("goal_apply", camera); +} + diff --git a/61_UI/AppScriptedValidation.cpp b/61_UI/AppScriptedValidation.cpp new file mode 100644 index 000000000..49a45e124 --- /dev/null +++ b/61_UI/AppScriptedValidation.cpp @@ -0,0 +1,77 @@ +#include "app/App.hpp" +void App::updateScriptedFollowVisualState(const CCameraScriptedFrameEvents& scriptedFrameEvents) +{ + if (!scriptedFrameEvents.trackedTargetTransforms.empty()) + { + setFollowTargetTransform(scriptedFrameEvents.trackedTargetTransforms.back().transform); + applyFollowToConfiguredCameras(true); + SCameraFollowVisualMetrics followMetrics = {}; + SActiveScriptedCameraContext runtimeContext = {}; + if (tryBuildActiveScriptedCameraContext(runtimeContext) && runtimeContext.followConfig) + { + followMetrics = nbl::system::CCameraFollowRegressionUtilities::buildFollowVisualMetrics( + runtimeContext.viewport.camera, + m_sceneInteraction.followTarget, + runtimeContext.followConfig, + runtimeContext.getProjectionContext()); + } + m_scriptedInput.visualFollow = followMetrics; + return; + } + + applyFollowToConfiguredCameras(); + m_scriptedInput.visualFollow = {}; +} + +void App::runActiveFrameScriptedChecks(const SScriptedFrameInputState& scriptedFrame) +{ + if (!(m_scriptedInput.enabled && m_scriptedInput.checkRuntime.nextCheckIndex < m_scriptedInput.timeline.checks.size())) + return; + + auto logFail = [&](const char* fmt, auto&&... args) -> void + { + m_scriptedInput.failed = true; + m_logger->log(fmt, ILogger::ELL_ERROR, std::forward(args)...); + }; + + auto logPass = [&](const char* fmt, auto&&... args) -> void + { + if (!m_scriptedInput.log) + return; + m_logger->log(fmt, ILogger::ELL_INFO, std::forward(args)...); + }; + + SActiveScriptedCameraContext runtimeContext = {}; + const bool hasRuntimeContext = tryBuildActiveScriptedCameraContext(runtimeContext); + + const auto checkResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( + m_scriptedInput.timeline.checks, + m_scriptedInput.checkRuntime, + { + .frame = m_realFrameIx, + .camera = hasRuntimeContext ? runtimeContext.viewport.camera : nullptr, + .imguizmoVirtual = scriptedFrame.imguizmoVirtualEvents.data(), + .imguizmoVirtualCount = static_cast(scriptedFrame.imguizmoVirtualEvents.size()), + .trackedTarget = &m_sceneInteraction.followTarget, + .followConfig = hasRuntimeContext ? runtimeContext.followConfig : nullptr, + .followProjectionContext = hasRuntimeContext ? runtimeContext.getProjectionContext() : nullptr, + .goalSolver = &m_cameraGoalSolver + }); + + for (const auto& entry : checkResult.logs) + { + if (entry.failure) + logFail("%s", entry.text.c_str()); + else + logPass("%s", entry.text.c_str()); + } + + if (!m_scriptedInput.summaryReported && m_scriptedInput.checkRuntime.nextCheckIndex >= m_scriptedInput.timeline.checks.size()) + { + m_scriptedInput.summaryReported = true; + if (m_scriptedInput.failed) + m_logger->log("[script] checks result: FAIL", ILogger::ELL_ERROR); + else + m_logger->log("[script] checks result: PASS", ILogger::ELL_INFO); + } +} diff --git a/61_UI/AppSpaceEnvironmentResources.cpp b/61_UI/AppSpaceEnvironmentResources.cpp new file mode 100644 index 000000000..b2df564b0 --- /dev/null +++ b/61_UI/AppSpaceEnvironmentResources.cpp @@ -0,0 +1,249 @@ +#include "app/App.hpp" + +#include "app/AppResourceUtilities.hpp" + +struct SSpaceEnvironmentTextureSpec final +{ + E_FORMAT format = EF_R16G16B16A16_SFLOAT; + asset::VkExtent3D extent = {}; + uint32_t mipLevels = 1u; + uint32_t arrayLayers = 1u; + std::array regions = {}; +}; + +inline SSpaceEnvironmentTextureSpec buildSpaceEnvironmentTextureSpec(const nbl::system::SSpaceEnvBlobHeader& envBlobHeader) +{ + SSpaceEnvironmentTextureSpec textureSpec = {}; + textureSpec.format = EF_R16G16B16A16_SFLOAT; + textureSpec.extent = { envBlobHeader.width, envBlobHeader.height, 1u }; + textureSpec.regions = {{ + { + .bufferOffset = 0ull, + .bufferRowLength = 0u, + .bufferImageHeight = 0u, + .imageSubresource = { + .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, + .mipLevel = 0u, + .baseArrayLayer = 0u, + .layerCount = textureSpec.arrayLayers + }, + .imageOffset = { 0, 0, 0 }, + .imageExtent = textureSpec.extent + } + }}; + return textureSpec; +} + +bool App::initializeSpaceEnvironmentResources() +{ + nbl::system::SSpaceEnvBlobHeader envBlobHeader = {}; + std::vector envBlobPayload; + nbl::system::loadPreferredSpaceEnvBlob(getCameraAppResourceContext(), envBlobHeader, envBlobPayload); + if (envBlobPayload.empty()) + return logFail("Failed to load space environment blob from available assets."); + + const auto textureSpec = buildSpaceEnvironmentTextureSpec(envBlobHeader); + + const auto createSpaceEnvironmentImage = [&]() -> bool + { + IGPUImage::SCreationParams imageParams = {}; + imageParams.type = IGPUImage::ET_2D; + imageParams.samples = IGPUImage::ESCF_1_BIT; + imageParams.format = textureSpec.format; + imageParams.extent = textureSpec.extent; + imageParams.mipLevels = textureSpec.mipLevels; + imageParams.arrayLayers = textureSpec.arrayLayers; + imageParams.flags = IGPUImage::ECF_NONE; + imageParams.usage = IGPUImage::EUF_SAMPLED_BIT | IGPUImage::EUF_TRANSFER_DST_BIT; + m_spaceEnvironment.image = m_device->createImage(std::move(imageParams)); + if (!m_spaceEnvironment.image) + return false; + + m_spaceEnvironment.image->setObjectDebugName("61_UI Space Environment"); + auto memReqs = m_spaceEnvironment.image->getMemoryReqs(); + memReqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); + return m_device->allocate(memReqs, m_spaceEnvironment.image.get()).isValid(); + }; + + const auto uploadSpaceEnvironmentImage = [&]() -> bool + { + auto uploadResult = m_utils->autoSubmit( + SIntendedSubmitInfo{ .queue = getGraphicsQueue() }, + [&](SIntendedSubmitInfo& submitInfo) -> bool + { + auto* recordingInfo = submitInfo.getCommandBufferForRecording(); + if (!recordingInfo) + return false; + + auto* cmdbuf = recordingInfo->cmdbuf; + using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; + const image_barrier_t preBarrier[] = {{ + .barrier = { + .dep = { + .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, + .srcAccessMask = ACCESS_FLAGS::NONE, + .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, + .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT + } + }, + .image = m_spaceEnvironment.image.get(), + .subresourceRange = { + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = textureSpec.mipLevels, + .baseArrayLayer = 0u, + .layerCount = textureSpec.arrayLayers + }, + .oldLayout = IGPUImage::LAYOUT::UNDEFINED, + .newLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL + }}; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo preDep = { .imgBarriers = preBarrier }; + bool success = cmdbuf->pipelineBarrier(asset::EDF_NONE, preDep); + success = success && m_utils->updateImageViaStagingBuffer( + submitInfo, + envBlobPayload.data(), + textureSpec.format, + m_spaceEnvironment.image.get(), + IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, + std::span(textureSpec.regions)); + + recordingInfo = submitInfo.getCommandBufferForRecording(); + if (!recordingInfo) + return false; + + cmdbuf = recordingInfo->cmdbuf; + const image_barrier_t postBarrier[] = {{ + .barrier = { + .dep = { + .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, + .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, + .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, + .dstAccessMask = ACCESS_FLAGS::SAMPLED_READ_BIT + } + }, + .image = m_spaceEnvironment.image.get(), + .subresourceRange = { + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = textureSpec.mipLevels, + .baseArrayLayer = 0u, + .layerCount = textureSpec.arrayLayers + }, + .oldLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, + .newLayout = IGPUImage::LAYOUT::READ_ONLY_OPTIMAL + }}; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo postDep = { .imgBarriers = postBarrier }; + return success && cmdbuf->pipelineBarrier(asset::EDF_NONE, postDep); + }); + return uploadResult.copy() == IQueue::RESULT::SUCCESS; + }; + + const auto createSpaceEnvironmentImageViewAndSampler = [&]() -> bool + { + IGPUImageView::SCreationParams viewParams = {}; + viewParams.subUsages = IGPUImage::EUF_SAMPLED_BIT; + viewParams.image = core::smart_refctd_ptr(m_spaceEnvironment.image); + viewParams.viewType = IGPUImageView::ET_2D; + viewParams.format = textureSpec.format; + viewParams.subresourceRange.aspectMask = IGPUImage::EAF_COLOR_BIT; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = textureSpec.mipLevels; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = textureSpec.arrayLayers; + m_spaceEnvironment.imageView = m_device->createImageView(std::move(viewParams)); + if (!m_spaceEnvironment.imageView) + return false; + + IGPUSampler::SParams samplerParams = {}; + samplerParams.MinFilter = ISampler::ETF_LINEAR; + samplerParams.MaxFilter = ISampler::ETF_LINEAR; + samplerParams.MipmapMode = ISampler::ESMM_LINEAR; + samplerParams.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; + samplerParams.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; + samplerParams.TextureWrapW = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; + samplerParams.AnisotropicFilter = 0u; + samplerParams.CompareEnable = false; + samplerParams.CompareFunc = ISampler::ECO_ALWAYS; + m_spaceEnvironment.sampler = m_device->createSampler(samplerParams); + return static_cast(m_spaceEnvironment.sampler); + }; + + const auto createSpaceEnvironmentPipelineAndDescriptors = [&]() -> bool + { + const IGPUDescriptorSetLayout::SBinding bindings[] = {{ + .binding = 0u, + .type = IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, + .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, + .stageFlags = IShader::E_SHADER_STAGE::ESS_FRAGMENT, + .count = 1u, + .immutableSamplers = &m_spaceEnvironment.sampler + }}; + m_spaceEnvironment.descriptorSetLayout = m_device->createDescriptorSetLayout(std::span{ bindings }); + if (!m_spaceEnvironment.descriptorSetLayout) + return false; + + const asset::SPushConstantRange pushConstantRange = { + .stageFlags = IShader::E_SHADER_STAGE::ESS_FRAGMENT, + .offset = 0u, + .size = sizeof(SpaceEnvPushConstants) + }; + auto pipelineLayout = m_device->createPipelineLayout( + { &pushConstantRange, 1u }, + core::smart_refctd_ptr(m_spaceEnvironment.descriptorSetLayout), + nullptr, + nullptr, + nullptr); + if (!pipelineLayout) + return false; + + const auto spaceFragKey = nbl::this_example::builtin::build::get_spirv_key<"sky_env_fragment">(m_device.get()); + auto fragmentShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), spaceFragKey); + if (!fragmentShader) + return false; + + nbl::ext::FullScreenTriangle::ProtoPipeline fsTriProto(m_assetMgr.get(), m_device.get(), m_logger.get()); + if (!fsTriProto) + return false; + + const IGPUPipelineBase::SShaderSpecInfo fragmentSpec = { + .shader = fragmentShader.get(), + .entryPoint = "main" + }; + m_spaceEnvironment.pipeline = fsTriProto.createPipeline(fragmentSpec, pipelineLayout.get(), m_debugScene.renderpass.get()); + if (!m_spaceEnvironment.pipeline) + return false; + + uint32_t setCount = 1u; + const IGPUDescriptorSetLayout* setLayouts[] = { m_spaceEnvironment.descriptorSetLayout.get() }; + m_spaceEnvironment.descriptorPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, setLayouts, &setCount); + if (!m_spaceEnvironment.descriptorPool) + return false; + + m_spaceEnvironment.descriptorSet = m_spaceEnvironment.descriptorPool->createDescriptorSet(core::smart_refctd_ptr(m_spaceEnvironment.descriptorSetLayout)); + if (!m_spaceEnvironment.descriptorSet) + return false; + + IGPUDescriptorSet::SDescriptorInfo info = {}; + info.desc = m_spaceEnvironment.imageView; + info.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + + IGPUDescriptorSet::SWriteDescriptorSet write = {}; + write.dstSet = m_spaceEnvironment.descriptorSet.get(); + write.binding = 0u; + write.arrayElement = 0u; + write.count = 1u; + write.info = &info; + return m_device->updateDescriptorSets({ &write, 1u }, {}); + }; + + if (!createSpaceEnvironmentImage()) + return logFail("Failed to create space environment image."); + if (!uploadSpaceEnvironmentImage()) + return logFail("Failed to upload space environment map."); + if (!createSpaceEnvironmentImageViewAndSampler()) + return logFail("Failed to create space environment image view or sampler."); + if (!createSpaceEnvironmentPipelineAndDescriptors()) + return logFail("Failed to initialize space environment pipeline resources."); + + return true; +} diff --git a/61_UI/AppTextResourceUtilities.cpp b/61_UI/AppTextResourceUtilities.cpp new file mode 100644 index 000000000..ffaee5784 --- /dev/null +++ b/61_UI/AppTextResourceUtilities.cpp @@ -0,0 +1,81 @@ +#include "app/AppResourceUtilities.hpp" + +#include "app/AppResourcePathUtilities.hpp" + +namespace nbl::system +{ + +bool tryLoadCameraConfigText( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraConfigLoadResult& outResult, + std::string* error) +{ + outResult = {}; + if (!context) + { + if (error) + *error = SCameraTextResourceErrorPrefixes::MissingContext; + return false; + } + + if (request.requestedPath) + { + std::string requestedError; + if (loadRequestedCameraConfigText( + *context.system, + context.localInputCWD, + request.requestedPath.value(), + outResult.text, + &outResult.loadedPath, + &requestedError)) + { + outResult.source = ECameraConfigLoadSource::RequestedPath; + return true; + } + + outResult.requestedPathLoadFailed = true; + outResult.requestedPathError = requestedError; + if (!request.fallbackToDefault) + { + if (error) + *error = outResult.requestedPathError; + return false; + } + } + + if (!loadDefaultCameraConfigText(*context.system, context.localInputCWD, outResult.text, &outResult.loadedPath, error)) + return false; + + outResult.source = ECameraConfigLoadSource::DefaultConfig; + return true; +} + +bool tryLoadCameraScriptText( + const SCameraAppResourceContext& context, + const path& scriptPath, + SCameraScriptTextLoadResult& outResult, + std::string* error) +{ + outResult = {}; + if (!context) + { + if (error) + *error = SCameraTextResourceErrorPrefixes::MissingContext; + return false; + } + + return loadTextResource( + *context.system, + context.localInputCWD, + { + .pathValue = scriptPath, + .lookupPolicy = EResourceLookupPolicy::RequestedPath, + .openErrorPrefix = SCameraTextResourceErrorPrefixes::ScriptedInput + }, + outResult.text, + &outResult.loadedPath, + error); +} + +} // namespace nbl::system diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp new file mode 100644 index 000000000..2fc585c77 --- /dev/null +++ b/61_UI/AppTransformEditor.cpp @@ -0,0 +1,124 @@ +#include "app/App.hpp" +#include "app/AppGizmoUtilities.hpp" + +void App::TransformEditorContents() +{ + const size_t objectsCount = getManipulableObjectCount(); + assert(objectsCount); + + int activeObject = static_cast(getManipulatedObjectIx()); + std::string activeObjectLabel = getManipulableObjectLabel(static_cast(activeObject)); + if (ImGui::BeginCombo("Active Object", activeObjectLabel.c_str())) + { + for (size_t i = 0u; i < objectsCount; ++i) + { + const bool isSelected = activeObject == static_cast(i); + const auto label = getManipulableObjectLabel(static_cast(i)); + if (ImGui::Selectable(label.c_str(), isSelected)) + { + activeObject = static_cast(i); + bindManipulatedObjectByIx(static_cast(activeObject)); + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + SManipulableObjectContext objectContext = {}; + if (!tryBuildManipulableObjectContext(static_cast(activeObject), objectContext)) + return; + + auto imguizmoModel = nbl::ui::makeImGuizmoModel(objectContext.transform); + float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; + + ImGui::Text("Identifier: \"%s\"", objectContext.label.c_str()); + if (ImGuizmo::IsUsingAny()) + ImGui::TextColored(SCameraAppTransformEditorUiDefaults::GizmoActiveStatusColor, "Gizmo: In Use"); + else + ImGui::TextColored(SCameraAppTransformEditorUiDefaults::GizmoIdleStatusColor, "Gizmo: Idle"); + + if (ImGui::IsItemHovered()) + { + nbl::ui::CCameraViewportOverlayUtilities::beginHoverInfoOverlay("HoverOverlay", ImGui::GetMousePos()); + ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); + nbl::ui::CCameraViewportOverlayUtilities::endHoverInfoOverlay(); + } + + ImGui::Separator(); + + if (objectContext.kind == SceneManipulatedObjectKind::Model) + { + const auto& names = m_debugScene.scene->getInitParams().geometryNames; + if (!names.empty()) + { + if (m_debugScene.geometrySelectionIx >= names.size()) + m_debugScene.geometrySelectionIx = 0; + + if (ImGui::BeginCombo("Object Type", names[m_debugScene.geometrySelectionIx].c_str())) + { + for (uint32_t i = 0u; i < names.size(); ++i) + { + const bool isSelected = (m_debugScene.geometrySelectionIx == i); + if (ImGui::Selectable(names[i].c_str(), isSelected)) + m_debugScene.geometrySelectionIx = static_cast(i); + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + } + + addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, m16TRSmatrix); + + if (ImGui::RadioButton("Translate", m_gizmoState.operation == ImGuizmo::TRANSLATE)) + m_gizmoState.operation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", m_gizmoState.operation == ImGuizmo::ROTATE)) + m_gizmoState.operation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", m_gizmoState.operation == ImGuizmo::SCALE)) + m_gizmoState.operation = ImGuizmo::SCALE; + + auto transformState = nbl::ui::extractRigidTransformComponentsOrDefault(imguizmoModel.outTRS); + + float32_t3 matrixRotation = hlsl::CCameraMathUtilities::getQuaternionEulerDegrees(transformState.orientation); + ImGui::InputFloat3("Tr", &transformState.translation[0], "%.3f"); + ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f"); + ImGui::InputFloat3("Sc", &transformState.scale[0], "%.3f"); + + imguizmoModel.outTRS = nbl::ui::composeRigidTransform( + transformState.translation, + matrixRotation, + transformState.scale); + m16TRSmatrix = &imguizmoModel.outTRS[0][0]; + + if (m_gizmoState.operation != ImGuizmo::SCALE) + { + if (ImGui::RadioButton("Local", m_gizmoState.mode == ImGuizmo::LOCAL)) + m_gizmoState.mode = ImGuizmo::LOCAL; + ImGui::SameLine(); + if (ImGui::RadioButton("World", m_gizmoState.mode == ImGuizmo::WORLD)) + m_gizmoState.mode = ImGuizmo::WORLD; + } + + ImGui::Checkbox(" ", &m_gizmoState.useSnap); + ImGui::SameLine(); + switch (m_gizmoState.operation) + { + case ImGuizmo::TRANSLATE: + ImGui::InputFloat3("Snap", &m_gizmoState.snap[0]); + break; + case ImGuizmo::ROTATE: + ImGui::InputFloat("Angle Snap", &m_gizmoState.snap[0]); + break; + case ImGuizmo::SCALE: + ImGui::InputFloat("Scale Snap", &m_gizmoState.snap[0]); + break; + } + + applyManipulableObjectTransform(objectContext, getCastedMatrix(imguizmoModel.outTRS)); +} diff --git a/61_UI/AppUiInputCapture.cpp b/61_UI/AppUiInputCapture.cpp new file mode 100644 index 000000000..def9b6c1f --- /dev/null +++ b/61_UI/AppUiInputCapture.cpp @@ -0,0 +1,88 @@ +#include "app/App.hpp" + +template +inline void appendFocusedChannelEvents( + ChannelReader& reader, + IWindow& window, + EventContainer& outEvents, + ILogger* logger) +{ + reader.consumeEvents([&](const auto& events) -> void + { + if (!window.hasInputFocus()) + return; + outEvents.insert(outEvents.end(), events.begin(), events.end()); + }, logger); +} + +inline void scaleCapturedMouseEvents( + std::vector& mouseEvents, + const CameraControlSettings& cameraControls) +{ + for (auto& event : mouseEvents) + { + if (event.type == ui::SMouseEvent::EET_SCROLL) + { + event.scrollEvent.verticalScroll *= cameraControls.mouseScrollScale; + event.scrollEvent.horizontalScroll *= cameraControls.mouseScrollScale; + continue; + } + + if (event.type == ui::SMouseEvent::EET_MOVEMENT) + { + event.movementEvent.relativeMovementX *= cameraControls.mouseMoveScale; + event.movementEvent.relativeMovementY *= cameraControls.mouseMoveScale; + } + } +} + +void App::updatePresentationTiming() +{ + m_inputSystem->getDefaultMouse(&mouse); + m_inputSystem->getDefaultKeyboard(&keyboard); + + oracle.reportEndFrameRecord(); + const auto timestamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + + m_nextPresentationTimestamp = timestamp; + if (m_presentationTiming.hasLastPresentationTimestamp) + { + const auto delta = m_nextPresentationTimestamp - m_presentationTiming.lastPresentationTimestamp; + if (delta.count() < 0) + m_presentationTiming.frameDeltaSec = 0.0; + else + m_presentationTiming.frameDeltaSec = std::chrono::duration(delta).count(); + } + m_presentationTiming.lastPresentationTimestamp = m_nextPresentationTimestamp; + m_presentationTiming.hasLastPresentationTimestamp = true; +} + +SCapturedUiEvents App::captureUiInputEvents() +{ + SCapturedUiEvents capturedEvents = {}; + appendFocusedChannelEvents(mouse, *m_window, capturedEvents.mouse, m_logger.get()); + appendFocusedChannelEvents(keyboard, *m_window, capturedEvents.keyboard, m_logger.get()); + return capturedEvents; +} + +void App::buildCameraInputEvents( + const SCapturedUiEvents& capturedEvents, + std::vector& outKeyboardEvents, + std::vector& outMouseEvents) const +{ + outKeyboardEvents = capturedEvents.keyboard; + outMouseEvents = capturedEvents.mouse; + scaleCapturedMouseEvents(outMouseEvents, m_cameraControls); +} + +nbl::ext::imgui::UI::SUpdateParameters App::buildUiUpdateParameters(const SCapturedUiEvents& capturedEvents) const +{ + const auto cursorPosition = m_window->getCursorControl()->getPosition(); + return { + .mousePosition = nbl::hlsl::float32_t2(cursorPosition.x, cursorPosition.y) - nbl::hlsl::float32_t2(m_window->getX(), m_window->getY()), + .displaySize = { m_window->getWidth(), m_window->getHeight() }, + .mouseEvents = { capturedEvents.mouse.data(), capturedEvents.mouse.size() }, + .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } + }; +} diff --git a/61_UI/AppUiResources.cpp b/61_UI/AppUiResources.cpp new file mode 100644 index 000000000..e04d0efac --- /dev/null +++ b/61_UI/AppUiResources.cpp @@ -0,0 +1,153 @@ +#include "app/App.hpp" +#include "app/AppResourceUtilities.hpp" + +template +struct SUiSampledDescriptorWrites final +{ + std::array descriptorInfo = {}; + std::array writes = {}; +}; + +template +inline void finalizeUiSampledDescriptorWrites(SUiSampledDescriptorWrites& output) +{ + for (uint32_t descriptorIx = 0u; descriptorIx < output.writes.size(); ++descriptorIx) + output.writes[descriptorIx].info = output.descriptorInfo.data() + descriptorIx; +} + +template +inline SUiSampledDescriptorWrites buildUiSampledDescriptorWrites( + nbl::ext::imgui::UI& uiManager, + IGPUDescriptorSet* descriptorSet, + std::span windowBindings) +{ + SUiSampledDescriptorWrites output = {}; + const auto fallbackView = core::smart_refctd_ptr(uiManager.getFontAtlasView()); + + for (uint32_t descriptorIx = 0u; descriptorIx < output.descriptorInfo.size(); ++descriptorIx) + { + output.descriptorInfo[descriptorIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + output.descriptorInfo[descriptorIx].desc = fallbackView; + } + + output.descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = fallbackView; + + for (uint32_t windowIx = 0u; windowIx < windowBindings.size(); ++windowIx) + { + const uint32_t textureIx = SCameraAppUiTextureSlots::viewport(windowIx); + output.descriptorInfo[textureIx].desc = + static_cast(windowBindings[windowIx].sceneColorView) ? + windowBindings[windowIx].sceneColorView : + fallbackView; + } + + for (uint32_t descriptorIx = 0u; descriptorIx < output.writes.size(); ++descriptorIx) + { + output.writes[descriptorIx].dstSet = descriptorSet; + output.writes[descriptorIx].binding = 0u; + output.writes[descriptorIx].arrayElement = descriptorIx; + output.writes[descriptorIx].count = 1u; + } + + return output; +} + +inline IDescriptorPool::SCreateInfo buildUiDescriptorPoolInfo(const uint32_t imageCount) +{ + IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = + static_cast(nbl::ext::imgui::UI::DefaultSamplerIx::COUNT); + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = + imageCount; + descriptorPoolInfo.maxSets = 1u; + descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; + return descriptorPoolInfo; +} + +template +inline void initializeViewportLayoutFromDisplaySize( + SAppWindowInitState& windowInit, + const float32_t2& displaySize) +{ + windowInit.trsEditor.iPos = SCameraAppViewportDefaults::WindowPaddingOffset; + windowInit.trsEditor.iSize = { 0.0f, displaySize.y - windowInit.trsEditor.iPos.y * 2 }; + + const float panelWidth = std::clamp( + displaySize.x * SCameraAppViewportLayoutDefaults::ControlPanelWidthRatio, + SCameraAppViewportLayoutDefaults::ControlPanelMinWidth, + displaySize.x * SCameraAppViewportLayoutDefaults::ControlPanelMaxWidthRatio); + windowInit.planars.iSize = { panelWidth, displaySize.y - SCameraAppViewportDefaults::WindowPaddingOffset.y * 2 }; + windowInit.planars.iPos = { + displaySize.x - windowInit.planars.iSize.x - SCameraAppViewportDefaults::WindowPaddingOffset.x, + SCameraAppViewportDefaults::WindowPaddingOffset.y + }; + + const float leftX = SCameraAppViewportLayoutDefaults::RenderPaddingX; + const float splitGap = SCameraAppViewportLayoutDefaults::SplitGap; + const float eachXSize = std::max(0.0f, displaySize.x - leftX * 2.0f); + const float eachYSize = + (displaySize.y - SCameraAppViewportLayoutDefaults::RenderPaddingY * 2.0f - (windowInit.renderWindows.size() - 1u) * splitGap) / + windowInit.renderWindows.size(); + + for (size_t windowIx = 0u; windowIx < windowInit.renderWindows.size(); ++windowIx) + { + auto& renderWindow = windowInit.renderWindows[windowIx]; + renderWindow.iPos = { + leftX, + SCameraAppViewportLayoutDefaults::RenderPaddingY + windowIx * (eachYSize + splitGap) + }; + renderWindow.iSize = { eachXSize, eachYSize }; + } +} + +bool App::updateGUIDescriptorSet() +{ + auto sampledWrites = buildUiSampledDescriptorWrites( + *m_ui.manager, + m_ui.descriptorSet.get(), + m_viewports.windowBindings); + finalizeUiSampledDescriptorWrites(sampledWrites); + return m_device->updateDescriptorSets(sampledWrites.writes, {}); +} + +bool App::initializeUiResources() +{ + nbl::ext::imgui::UI::SCreationParameters params; + params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; + params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; + params.assetManager = m_assetMgr; + params.pipelineCache = nullptr; + params.pipelineLayout = nbl::ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, TotalUISampleTexturesAmount); + params.renderpass = smart_refctd_ptr(m_renderpass); + params.subpassIx = 0u; + params.transfer = getTransferUpQueue(); + params.utilities = m_utils; + + const auto imguiKey = nbl::this_example::builtin::build::get_spirv_key<"imgui.unified">(m_device.get()); + auto imguiShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), imguiKey); + if (!imguiShader) + return logFail("Failed to load precompiled ImGui shaders."); + + params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ + .vertex = imguiShader, + .fragment = std::move(imguiShader) + }; + + m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + if (!m_ui.manager) + return false; + + const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); + m_descriptorSetPool = m_device->createDescriptorPool(buildUiDescriptorPoolInfo(TotalUISampleTexturesAmount)); + assert(m_descriptorSetPool); + + m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); + assert(m_ui.descriptorSet); + + m_ui.manager->registerListener([this]() -> void { imguiListen(); }); + + const auto displaySize = float32_t2{ m_window->getWidth(), m_window->getHeight() }; + initializeViewportLayoutFromDisplaySize(m_viewports.windowInit, displaySize); + + return true; +} diff --git a/61_UI/AppUiRuntime.cpp b/61_UI/AppUiRuntime.cpp new file mode 100644 index 000000000..1b8957700 --- /dev/null +++ b/61_UI/AppUiRuntime.cpp @@ -0,0 +1,243 @@ +#include "app/App.hpp" + +void App::paceScriptedVisualDebugFrame() +{ + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + { + m_scriptedInput.framePacer.initialized = false; + return; + } + + if (m_scriptedInput.visualTargetFps <= 0.f) + return; + + const auto frameDuration = std::chrono::duration_cast( + std::chrono::duration(1.0 / static_cast(m_scriptedInput.visualTargetFps))); + const auto now = std::chrono::steady_clock::now(); + + if (!m_scriptedInput.framePacer.initialized) + { + m_scriptedInput.framePacer.initialized = true; + m_scriptedInput.framePacer.nextFrame = now + frameDuration; + return; + } + + if (now < m_scriptedInput.framePacer.nextFrame) + std::this_thread::sleep_until(m_scriptedInput.framePacer.nextFrame); + + auto postSleepNow = std::chrono::steady_clock::now(); + while (m_scriptedInput.framePacer.nextFrame < postSleepNow) + m_scriptedInput.framePacer.nextFrame += frameDuration; +} + +bool App::keepRunning() +{ + if (m_cliRuntime.headlessCameraSmokeMode) + return false; + + if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) + { + if (!m_cliRuntime.ciMode || m_cliRuntime.ciScreenshotDone) + std::exit(EXIT_FAILURE); + } + + if (m_cliRuntime.ciMode && m_cliRuntime.ciStartedAt != clock_t::time_point::min()) + { + const auto elapsed = clock_t::now() - m_cliRuntime.ciStartedAt; + if (elapsed > SCameraAppRuntimeDefaults::CiMaxRuntime) + { + m_logger->log( + "[ci][fail] watchdog timeout after %.2f s.", + ILogger::ELL_ERROR, + std::chrono::duration(elapsed).count()); + std::exit(EXIT_FAILURE); + } + } + + if (m_cliRuntime.ciMode && m_cliRuntime.ciScreenshotDone) + { + if (m_scriptedInput.enabled) + { + if (m_scriptedInput.nextCaptureIndex < m_scriptedInput.timeline.captureFrames.size()) + return true; + if (m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) + return true; + if (m_scriptedInput.checkRuntime.nextCheckIndex < m_scriptedInput.timeline.checks.size()) + return true; + } + return false; + } + + return !m_surface->irrecoverable(); +} + +bool App::onAppTerminated() +{ + if (m_cliRuntime.headlessCameraSmokeMode) + return m_cliRuntime.headlessCameraSmokePassed; + + return base_t::onAppTerminated(); +} + +void App::syncWindowInputBinding(SWindowControlBinding& binding) +{ + if (!binding.boundProjectionIx.has_value()) + return; + if (binding.activePlanarIx >= m_planarProjections.size()) + return; + + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + return; + + const auto projectionIx = binding.boundProjectionIx.value(); + auto& projections = planar->getPlanarProjections(); + if (projectionIx >= projections.size()) + return; + + if (binding.inputBindingPlanarIx == binding.activePlanarIx && binding.inputBindingProjectionIx == projectionIx) + return; + + binding.inputBinding.copyBindingLayoutFrom(projections[projectionIx].getInputBinding()); + binding.inputBindingPlanarIx = binding.activePlanarIx; + binding.inputBindingProjectionIx = projectionIx; +} + +void App::syncWindowInputBindingToProjection(SWindowControlBinding& binding) +{ + if (!binding.boundProjectionIx.has_value()) + return; + if (binding.activePlanarIx >= m_planarProjections.size()) + return; + + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + return; + + const auto projectionIx = binding.boundProjectionIx.value(); + auto& projections = planar->getPlanarProjections(); + if (projectionIx >= projections.size()) + return; + + projections[projectionIx].getInputBinding().copyBindingLayoutFrom(binding.inputBinding); + binding.inputBindingPlanarIx = binding.activePlanarIx; + binding.inputBindingProjectionIx = projectionIx; +} + +bool App::shouldCaptureOSCursor() +{ + if (!m_viewports.enableActiveCameraMovement || !m_viewports.captureCursorInMoveMode) + return false; + if (m_cliRuntime.ciMode || m_scriptedInput.enabled) + return false; + if (!m_window || !m_window->hasInputFocus() || !m_window->hasMouseFocus()) + return false; + return true; +} + +void App::UpdateBoundCameraMovement() +{ + ImGuiIO& io = ImGui::GetIO(); + + if (ImGui::IsKeyPressed(ImGuiKey_Space)) + m_viewports.enableActiveCameraMovement = !m_viewports.enableActiveCameraMovement; + + if (m_viewports.enableActiveCameraMovement) + { + io.ConfigFlags |= ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = false; + io.WantCaptureMouse = false; + + if (shouldCaptureOSCursor()) + { + const ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + if (cc) + { + const int32_t posX = m_window->getX(); + const int32_t posY = m_window->getY(); + + if (m_viewports.resetCursorToCenter) + { + const ICursorControl::SPosition middle{ + static_cast(viewportSize.x / 2 + posX), + static_cast(viewportSize.y / 2 + posY) + }; + cc->setPosition(middle); + } + else + { + const auto currentCursorPos = cc->getPosition(); + ICursorControl::SPosition newPos{}; + newPos.x = std::clamp(currentCursorPos.x, posX, static_cast(viewportSize.x) + posX); + newPos.y = std::clamp(currentCursorPos.y, posY, static_cast(viewportSize.y) + posY); + cc->setPosition(newPos); + } + } + } + } + else + { + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = false; + io.WantCaptureMouse = true; + } +} + +void App::UpdateCursorVisibility() +{ + auto* cc = m_window ? m_window->getCursorControl() : nullptr; + if (!cc) + return; + + cc->setVisible(!shouldCaptureOSCursor()); +} + +void App::UpdateUiMetrics() +{ + m_uiMetrics.lastFrameMs = static_cast(m_presentationTiming.frameDeltaSec * 1000.0); + m_uiMetrics.lastInputEvents = m_uiMetrics.inputEventsThisFrame; + m_uiMetrics.lastVirtualEvents = m_uiMetrics.virtualEventsThisFrame; + + m_uiMetrics.frameMs[m_uiMetrics.sampleIndex] = m_uiMetrics.lastFrameMs; + m_uiMetrics.inputCounts[m_uiMetrics.sampleIndex] = static_cast(m_uiMetrics.inputEventsThisFrame); + m_uiMetrics.virtualCounts[m_uiMetrics.sampleIndex] = static_cast(m_uiMetrics.virtualEventsThisFrame); + + m_uiMetrics.sampleIndex = (m_uiMetrics.sampleIndex + 1u) % SCameraAppRuntimeDefaults::UiMetricSamples; + m_uiMetrics.inputEventsThisFrame = 0u; + m_uiMetrics.virtualEventsThisFrame = 0u; +} + +void App::addMatrixTable(const char* topText, const char* tableName, const int rows, const int columns, const float* pointer, const bool withSeparator) +{ + ImGui::Text(topText); + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)); + if (ImGui::BeginTable(tableName, columns, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame)) + { + for (int y = 0; y < rows; ++y) + { + ImGui::TableNextRow(); + for (int x = 0; x < columns; ++x) + { + ImGui::TableSetColumnIndex(x); + if (pointer) + ImGui::Text("%.3f", *(pointer + (y * columns) + x)); + else + ImGui::Text("-"); + } + } + ImGui::EndTable(); + } + ImGui::PopStyleColor(2); + if (withSeparator) + ImGui::Separator(); +} + +void App::finalizeUiFrameState() +{ + UpdateBoundCameraMovement(); + UpdateCursorVisibility(); + applyFollowToConfiguredCameras(); + refreshViewportBindingMatrices(); +} diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp new file mode 100644 index 000000000..779fd6102 --- /dev/null +++ b/61_UI/AppUpdate.cpp @@ -0,0 +1,114 @@ +#include "app/App.hpp" + +inline void logScriptedFramePayload( + ILogger& logger, + const uint64_t frameIx, + const SScriptedFrameInputState& scriptedFrame) +{ + logger.log( + "[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu goals=%zu target=%zu", + ILogger::ELL_INFO, + static_cast(frameIx), + scriptedFrame.keyboard.size(), + scriptedFrame.mouse.size(), + scriptedFrame.frameEvents.imguizmo.size(), + scriptedFrame.frameEvents.goals.size(), + scriptedFrame.frameEvents.trackedTargetTransforms.size()); +} + +SAppFrameUpdateState App::buildFrameUpdateState() +{ + SAppFrameUpdateState frameState = {}; + prepareScriptedFrameState(frameState.scripted); + prepareCameraAndUiInput(frameState.scripted, frameState.cameraInput, frameState.ui); + return frameState; +} + +void App::prepareScriptedFrameState(SAppFrameUpdateState::SPreparedScriptedFrame& outState) +{ + outState = {}; + outState.skipCameraInput = m_playbackAuthoring.playback.playing && m_playbackAuthoring.playback.overrideInput; + dequeueScriptedFrameInput(outState.frame); + applyScriptedFrameActions(outState.frame.actions); + ensureScriptedVisualPlanarState(); +} + +void App::prepareCameraAndUiInput( + const SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState, + SAppFrameUpdateState::SPreparedCapturedInput& outCameraInput, + SAppFrameUpdateState::SUiRuntimeState& outUiState) +{ + prepareCapturedCameraInput(scriptedState, outCameraInput); + prepareUiRuntimeState(outCameraInput, outUiState); +} + +void App::prepareCapturedCameraInput( + const SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState, + SAppFrameUpdateState::SPreparedCapturedInput& outCameraInput) +{ + outCameraInput = {}; + outCameraInput.capturedEvents = captureUiInputEvents(); + if (m_scriptedInput.enabled && m_scriptedInput.exclusive) + outCameraInput.capturedEvents.clear(); + + appendScriptedInputEvents(scriptedState.frame, outCameraInput.capturedEvents); + m_uiMetrics.inputEventsThisFrame = outCameraInput.capturedEvents.getEventCount(); + buildCameraInputEvents( + outCameraInput.capturedEvents, + outCameraInput.keyboardEvents, + outCameraInput.mouseEvents); +} + +void App::prepareUiRuntimeState( + const SAppFrameUpdateState::SPreparedCapturedInput& cameraInput, + SAppFrameUpdateState::SUiRuntimeState& outUiState) +{ + outUiState = {}; + outUiState.updateParams = buildUiUpdateParameters(cameraInput.capturedEvents); +} + +void App::runCameraFramePasses(SAppFrameUpdateState& frameState) +{ + applyPreparedCameraInput(frameState.cameraInput, frameState.scripted.skipCameraInput); + runPreparedScriptedFrame(frameState.scripted); +} + +void App::applyPreparedCameraInput( + const SAppFrameUpdateState::SPreparedCapturedInput& cameraInput, + const bool skipCameraInput) +{ + applyActiveCameraInput(cameraInput.keyboardEvents, cameraInput.mouseEvents, skipCameraInput); +} + +void App::runPreparedScriptedFrame(SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState) +{ + if (m_scriptedInput.log && scriptedState.frame.hasRuntimePayload()) + logScriptedFramePayload(*m_logger, m_realFrameIx, scriptedState.frame); + + applyScriptedImguizmoInput(scriptedState.frame, scriptedState.skipCameraInput); + applyScriptedGoals(scriptedState.frame.frameEvents, scriptedState.skipCameraInput); + updateScriptedFollowVisualState(scriptedState.frame.frameEvents); + runActiveFrameScriptedChecks(scriptedState.frame); +} + +void App::updateUiFrame(const SAppFrameUpdateState::SUiRuntimeState& uiState) +{ + UpdateUiMetrics(); + m_ui.manager->update(uiState.updateParams); +} + +void App::applyFrameRuntimeState(SAppFrameUpdateState& frameState) +{ + runCameraFramePasses(frameState); + updateUiFrame(frameState.ui); +} + +void App::update() +{ + updatePresentationTiming(); + updatePlayback(m_presentationTiming.frameDeltaSec); + + auto frameState = buildFrameUpdateState(); + applyFrameRuntimeState(frameState); +} + diff --git a/61_UI/AppViewportGizmos.cpp b/61_UI/AppViewportGizmos.cpp new file mode 100644 index 000000000..96659044e --- /dev/null +++ b/61_UI/AppViewportGizmos.cpp @@ -0,0 +1,104 @@ +#include "app/App.hpp" +#include "app/AppGizmoUtilities.hpp" + +void App::drawViewportManipulationGizmos( + uint32_t windowIx, + SWindowControlBinding& binding, + const nbl::ui::SBoundViewportCameraState& viewportState, + size_t& gizmoIx) +{ + for (uint32_t objectIx = 0u; objectIx < getManipulableObjectCount(); ++objectIx) + { + ImGuizmo::PushID(gizmoIx); + ++gizmoIx; + + SManipulableObjectContext objectContext = {}; + if (!tryBuildManipulableObjectContext(objectIx, objectContext)) + { + ImGuizmo::PopID(); + continue; + } + + if (objectContext.camera == viewportState.camera) + { + ImGuizmo::PopID(); + continue; + } + + auto imguizmoModel = nbl::ui::makeImGuizmoModel(objectContext.transform); + + const float gizmoWorldRadius = objectContext.isFollowTarget() ? SCameraAppViewportDefaults::FollowTargetGizmoWorldRadius : SCameraAppViewportDefaults::DefaultGizmoWorldRadius; + const float gizmoSizeClip = nbl::ui::computeViewportGizmoClipSize( + viewportState, + objectContext.worldPosition, + gizmoWorldRadius); + ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); + + const bool success = ImGuizmo::Manipulate( + &viewportState.imguizmoPlanar.view[0][0], + &viewportState.imguizmoPlanar.projection[0][0], + ImGuizmo::OPERATION::UNIVERSAL, + m_gizmoState.mode, + &imguizmoModel.outTRS[0][0], + &imguizmoModel.outDeltaTRS[0][0], + m_gizmoState.useSnap ? &m_gizmoState.snap[0] : nullptr); + + if (success) + { + bindManipulableObject(objectContext); + applyManipulableObjectTransform(objectContext, getCastedMatrix(imguizmoModel.outTRS)); + } + + if (ImGuizmo::IsOver() && !ImGuizmo::IsUsingAny() && !m_viewports.enableActiveCameraMovement) + { + if (objectContext.isCamera() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + const uint32_t newPlanarIx = objectContext.planarIx.value(); + if (nbl::ui::trySelectBindingPlanar( + getPlanarProjectionSpan(), + binding, + newPlanarIx)) + { + updateActiveRenderWindowFromViewport(windowIx, false, true); + } + } + else if (objectContext.isFollowTarget() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + bindManipulableObject(objectContext); + } + else if (!objectContext.isCamera() && !objectContext.isFollowTarget() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + bindManipulableObject(objectContext); + } + + drawManipulableObjectHoverOverlay(objectContext); + } + + ImGuizmo::PopID(); + } +} + +void App::drawManipulableObjectHoverOverlay(const SManipulableObjectContext& objectContext) const +{ + const ImVec2 mousePos = ImGui::GetIO().MousePos; + nbl::ui::CCameraViewportOverlayUtilities::beginHoverInfoOverlay("InfoOverlay", mousePos); + + ImGui::Text("Identifier: %s", objectContext.label.c_str()); + ImGui::Text("Object Ix: %u", objectContext.objectIx); + if (objectContext.isCamera()) + { + ImGui::Separator(); + ImGui::TextDisabled("RMB: switch view to this camera"); + ImGui::TextDisabled("LMB drag: manipulate gizmo"); + ImGui::TextDisabled("SPACE: toggle move mode"); + } + else if (objectContext.isFollowTarget()) + { + ImGui::Separator(); + ImGui::TextDisabled("RMB: select follow target"); + ImGui::TextDisabled("LMB drag: move or rotate tracked target"); + ImGui::TextDisabled("Enabled follow cameras update on the next frame"); + } + + nbl::ui::CCameraViewportOverlayUtilities::endHoverInfoOverlay(); +} diff --git a/61_UI/AppViewportWindows.cpp b/61_UI/AppViewportWindows.cpp new file mode 100644 index 000000000..55d957c7e --- /dev/null +++ b/61_UI/AppViewportWindows.cpp @@ -0,0 +1,151 @@ +#include "app/App.hpp" +#include "app/AppViewportBindingUtilities.hpp" +#include "app/AppViewportWindowUtilities.hpp" + +void App::drawWindowedViewportWindows(ImGuiIO& io, SImResourceInfo& info) +{ + syncVisualDebugWindowBindings(); + const bool hideSceneGizmos = m_viewports.enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); + ImGuizmo::Enable(!hideSceneGizmos); + + size_t gizmoIx = 0u; + const ImGuiCond windowCond = m_cliRuntime.ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; + + for (uint32_t windowIx = 0u; windowIx < m_viewports.windowBindings.size(); ++windowIx) + drawWindowedViewportWindow(windowIx, windowCond, hideSceneGizmos, gizmoIx, info); + + if (m_viewports.windowBindings.size() > 1u) + drawViewportSplitOverlayWindow(io.DisplaySize); +} + +void App::drawWindowedViewportWindow(uint32_t windowIx, ImGuiCond windowCond, bool hideSceneGizmos, size_t& gizmoIx, SImResourceInfo& info) +{ + const auto& rw = m_viewports.windowInit.renderWindows[windowIx]; + ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); + ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); + ImGui::SetNextWindowSizeConstraints(SCameraAppViewportDefaults::MinWindowSize, SCameraAppViewportDefaults::MaxWindowSize); + + nbl::ui::CCameraViewportOverlayUtilities::pushViewportWindowStyle(); + const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; + + ImGui::Begin(ident.data(), nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus); + auto& binding = m_viewports.windowBindings[windowIx]; + nbl::ui::SViewportWindowRuntime viewportRuntime = {}; + const auto planarSpan = getPlanarProjectionSpan(); + const bool viewportValid = nbl::ui::tryBuildViewportWindowRuntime(planarSpan, binding, SCameraAppViewportDefaults::FlipGizmoY, viewportRuntime); + const auto& frame = viewportRuntime.frame; + + if (ImGuiWindow* const window = ImGui::GetCurrentWindow()) + nbl::ui::updateViewportWindowMoveFlag(window, frame); + + if (!viewportValid) + { + ImGui::End(); + nbl::ui::CCameraViewportOverlayUtilities::popViewportWindowStyle(); + return; + } + const auto& viewportState = viewportRuntime.viewportState; + + auto& projection = *viewportState.projection; + info.textureID = SCameraAppUiTextureSlots::viewport(windowIx); + + ImGuizmo::AllowAxisFlip(binding.allowGizmoAxesToFlip); + ImGuizmo::SetOrthographic(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic); + ImGuizmo::SetDrawlist(); + nbl::ui::drawViewportTextureAndOverlay( + info, + viewportRuntime, + m_sceneInteraction.followTarget, + m_scriptedInput, + [&](ImDrawList& drawList, const nbl::ui::SViewportOverlayRect& viewportRect, const nbl::ui::SBoundViewportCameraState& state) + { + drawViewportWindowOverlay(drawList, viewportRect, windowIx, binding, state); + }); + + updateActiveRenderWindowFromViewport(windowIx, frame.hovered, frame.focused); + + if (!hideSceneGizmos) + drawViewportManipulationGizmos(windowIx, binding, viewportState, gizmoIx); + + ImGui::End(); + nbl::ui::CCameraViewportOverlayUtilities::popViewportWindowStyle(); +} + +void App::drawViewportWindowOverlay( + ImDrawList& drawList, + const nbl::ui::SViewportOverlayRect& viewportRect, + uint32_t windowIx, + const SWindowControlBinding& binding, + const nbl::ui::SBoundViewportCameraState& viewportState) const +{ + const char* projLabel = viewportState.projection->getParameters().m_type == IPlanarProjection::CProjection::Perspective ? "Persp" : "Ortho"; + nbl::ui::SCameraViewportInfoOverlayData overlayData = {}; + overlayData.headline = "Planar " + std::to_string(binding.activePlanarIx) + " | " + projLabel + " | W" + std::to_string(windowIx); + overlayData.description = std::string(CCameraTextUtilities::getCameraTypeLabel(viewportState.camera)) + ": " + std::string(CCameraTextUtilities::getCameraTypeDescription(viewportState.camera)); + nbl::ui::CCameraViewportOverlayUtilities::drawViewportInfoOverlay(drawList, viewportRect, overlayData); +} + +void App::updateActiveRenderWindowFromViewport(uint32_t windowIx, bool windowHovered, bool windowFocused) +{ + if (m_scriptedInput.enabled && m_scriptedInput.exclusive) + return; + + if (!m_scriptedInput.enabled && windowHovered) + m_viewports.activeRenderWindowIx = windowIx; + else if (windowFocused) + m_viewports.activeRenderWindowIx = windowIx; +} + +void App::drawViewportSplitOverlayWindow(const ImVec2& displaySize) +{ + const auto& topRw = m_viewports.windowInit.renderWindows[0]; + const float splitY = topRw.iPos.y + topRw.iSize.y; + const float gap = std::max(0.0f, m_viewports.windowInit.renderWindows[1].iPos.y - splitY); + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(displaySize, ImGuiCond_Always); + ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + nbl::ui::CCameraViewportOverlayUtilities::drawViewportSplitOverlay(*drawList, displaySize, splitY, gap); + ImGui::End(); +} + +void App::drawFullscreenViewportWindow(ImGuiIO& io, SImResourceInfo& info) +{ + info.textureID = SCameraAppUiTextureSlots::viewport(m_viewports.activeRenderWindowIx); + + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); + ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::PushStyleColor(ImGuiCol_WindowBg, nbl::ui::SCameraViewportWindowStyle::WindowBackgroundColor); + ImGui::Begin("FullScreenWindow", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs); + nbl::ui::SViewportWindowRuntime viewportRuntime = {}; + const auto planarSpan = getPlanarProjectionSpan(); + const bool viewportValid = nbl::ui::tryBuildViewportWindowRuntime(planarSpan, m_viewports.windowBindings[m_viewports.activeRenderWindowIx], false, viewportRuntime); + if (viewportValid) + { + nbl::ui::drawViewportTextureAndOverlay( + info, + viewportRuntime, + m_sceneInteraction.followTarget, + m_scriptedInput, + [](ImDrawList&, const nbl::ui::SViewportOverlayRect&, const nbl::ui::SBoundViewportCameraState&) {}); + } + else + { + const auto& frame = viewportRuntime.frame; + ImGui::Image(info, frame.contentRegionSize); + ImGuizmo::SetRect(frame.cursorPos.x, frame.cursorPos.y, frame.contentRegionSize.x, frame.contentRegionSize.y); + } + + ImGui::End(); + ImGui::PopStyleColor(1); +} + +void App::refreshViewportBindingMatrices() +{ + const auto planarSpan = getPlanarProjectionSpan(); + for (auto& binding : m_viewports.windowBindings) + { + nbl::ui::SBoundViewportCameraState viewportState = {}; + nbl::ui::tryBuildWindowBindingMatrices(planarSpan, binding, viewportState); + } +} diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp new file mode 100644 index 000000000..3f0d2fa9e --- /dev/null +++ b/61_UI/AppWorkLoop.cpp @@ -0,0 +1,19 @@ +#include "app/App.hpp" + +void App::workLoopBody() +{ + paceScriptedVisualDebugFrame(); + if (!waitForInflightFrameSlot()) + return; + + auto frameContext = tryBuildFrameSubmissionContext(); + if (!frameContext.has_value()) + return; + + update(); + if (!recordFramePasses(*frameContext)) + return; + (void)submitAndPresentFrame(*frameContext); +} + + diff --git a/61_UI/CCameraScriptedRuntimePersistence.cpp b/61_UI/CCameraScriptedRuntimePersistence.cpp new file mode 100644 index 000000000..54569f268 --- /dev/null +++ b/61_UI/CCameraScriptedRuntimePersistence.cpp @@ -0,0 +1,646 @@ +#include "camera/CCameraScriptedRuntimePersistence.hpp" + +#include +#include +#include +#include + +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CCameraVirtualEventUtilities.hpp" +#include "nlohmann/json.hpp" + +using json_t = nlohmann::json; + +namespace nbl::this_example +{ + +namespace impl +{ + +template +void readVector3(const json_t& entry, T& outValue) +{ + using scalar_t = std::remove_reference_t; + const auto values = entry.get>(); + outValue = T(values[0], values[1], values[2]); +} + +nbl::hlsl::float32_t4x4 composeScriptedImguizmoTransform( + const std::array& translation, + const std::array& rotationDeg, + const std::array& scale) +{ + return nbl::hlsl::CCameraMathUtilities::composeTransformMatrix( + nbl::hlsl::float32_t3(translation[0], translation[1], translation[2]), + nbl::hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegrees(nbl::hlsl::float32_t3(rotationDeg[0], rotationDeg[1], rotationDeg[2])), + nbl::hlsl::float32_t3(scale[0], scale[1], scale[2])); +} + +nbl::hlsl::float32_t4x4 makeScriptedMatrixFromArray(const std::array& values) +{ + nbl::hlsl::float32_t4x4 out(1.f); + for (uint32_t column = 0u; column < 4u; ++column) + { + for (uint32_t row = 0u; row < 4u; ++row) + out[column][row] = values[column * 4u + row]; + } + return out; +} + +std::optional parseScriptedKeyboardAction(std::string_view action) +{ + if (action == "pressed" || action == "press") + return nbl::system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed; + if (action == "released" || action == "release") + return nbl::system::CCameraScriptedInputEvent::KeyboardData::Action::Released; + return std::nullopt; +} + +nbl::ui::E_KEY_CODE parseScriptedKeyCode(std::string_view key) +{ + auto parsed = nbl::ui::stringToKeyCode(key); + if (parsed != nbl::ui::EKC_NONE) + return parsed; + + constexpr std::string_view KeyPrefix = "KEY_"; + constexpr std::string_view EkcPrefix = "EKC_"; + if (key.starts_with(KeyPrefix)) + parsed = nbl::ui::stringToKeyCode(key.substr(KeyPrefix.size())); + if (parsed == nbl::ui::EKC_NONE && key.starts_with(EkcPrefix)) + parsed = nbl::ui::stringToKeyCode(key.substr(EkcPrefix.size())); + return parsed; +} + +std::optional parseScriptedMouseButton(std::string_view button) +{ + auto tryParseCode = [](std::string_view code) -> std::optional + { + switch (nbl::ui::stringToMouseCode(code)) + { + case nbl::ui::EMC_LEFT_BUTTON: + return nbl::ui::EMB_LEFT_BUTTON; + case nbl::ui::EMC_RIGHT_BUTTON: + return nbl::ui::EMB_RIGHT_BUTTON; + case nbl::ui::EMC_MIDDLE_BUTTON: + return nbl::ui::EMB_MIDDLE_BUTTON; + default: + return std::nullopt; + } + }; + + auto parsed = tryParseCode(button); + if (parsed.has_value()) + return parsed; + + constexpr std::string_view ButtonPrefix = "BUTTON_"; + constexpr std::string_view EmbPrefix = "EMB_"; + if (button.starts_with(ButtonPrefix)) + parsed = tryParseCode(button.substr(ButtonPrefix.size())); + if (!parsed.has_value() && button.starts_with(EmbPrefix)) + parsed = tryParseCode(button.substr(EmbPrefix.size())); + + return parsed; +} + +std::optional parseScriptedMouseClickAction(std::string_view action) +{ + if (action == "pressed" || action == "press") + return nbl::system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed; + if (action == "released" || action == "release") + return nbl::system::CCameraScriptedInputEvent::MouseData::ClickAction::Released; + return std::nullopt; +} + +void appendScriptedCaptureFrame( + nbl::this_example::CCameraScriptedInputParseResult& out, + const uint64_t frame, + const bool captureFrame) +{ + if (captureFrame) + out.timeline.captureFrames.emplace_back(frame); +} + +void parseScriptedCaptureFramesJson(const json_t& script, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("capture_frames") || !script["capture_frames"].is_array()) + return; + + for (const auto& entry : script["capture_frames"]) + { + if (entry.is_number_unsigned()) + out.timeline.captureFrames.emplace_back(entry.get()); + } +} + +void parseScriptedControlOverridesJson(const json_t& controls, nbl::this_example::CCameraScriptedControlOverrides& out) +{ + if (!controls.is_object()) + return; + + if (controls.contains("keyboard_scale")) + { + out.keyboardScale = controls["keyboard_scale"].get(); + out.hasKeyboardScale = true; + } + if (controls.contains("mouse_move_scale")) + { + out.mouseMoveScale = controls["mouse_move_scale"].get(); + out.hasMouseMoveScale = true; + } + if (controls.contains("mouse_scroll_scale")) + { + out.mouseScrollScale = controls["mouse_scroll_scale"].get(); + out.hasMouseScrollScale = true; + } + if (controls.contains("translation_scale")) + { + out.translationScale = controls["translation_scale"].get(); + out.hasTranslationScale = true; + } + if (controls.contains("rotation_scale")) + { + out.rotationScale = controls["rotation_scale"].get(); + out.hasRotationScale = true; + } +} + +bool parseScriptedSequenceIfPresentJson(const json_t& script, nbl::this_example::CCameraScriptedInputParseResult& out, std::string* error) +{ + nbl::core::CCameraSequenceScript sequence; + if (script.contains("segments")) + { + if (!nbl::system::CCameraSequenceScriptPersistenceUtilities::deserializeCameraSequenceScript(script.dump(), sequence, error)) + return false; + out.sequence = std::move(sequence); + return true; + } + + if (script.contains("sequence")) + { + if (!nbl::system::CCameraSequenceScriptPersistenceUtilities::deserializeCameraSequenceScript(script["sequence"].dump(), sequence, error)) + return false; + out.sequence = std::move(sequence); + } + + return true; +} + +void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("key") || !event.contains("action")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); + return; + } + + const auto keyText = event["key"].get(); + const auto actionText = event["action"].get(); + const auto key = parseScriptedKeyCode(keyText); + if (key == nbl::ui::EKC_NONE) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); + return; + } + + const auto action = parseScriptedKeyboardAction(actionText); + if (!action.has_value()) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionText + "\"."); + return; + } + + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Keyboard; + entry.keyboard.key = key; + entry.keyboard.action = action.value(); + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("kind")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); + return; + } + + const auto kind = event["kind"].get(); + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Mouse; + + if (kind == "move") + { + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Movement; + entry.mouse.delta = nbl::hlsl::int16_t2(event.value("dx", 0), event.value("dy", 0)); + } + else if (kind == "scroll") + { + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Scroll; + entry.mouse.scroll = nbl::hlsl::int16_t2(event.value("v", 0), event.value("h", 0)); + } + else if (kind == "click") + { + if (!event.contains("button") || !event.contains("action")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); + return; + } + + const auto buttonText = event["button"].get(); + const auto actionText = event["action"].get(); + const auto button = parseScriptedMouseButton(buttonText); + if (!button.has_value()) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); + return; + } + + const auto action = parseScriptedMouseClickAction(actionText); + if (!action.has_value()) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionText + "\"."); + return; + } + + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Click; + entry.mouse.button = button.value(); + entry.mouse.action = action.value(); + entry.mouse.position = nbl::hlsl::int16_t2(event.value("x", 0), event.value("y", 0)); + } + else + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); + return; + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedImguizmoEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Imguizmo; + + if (event.contains("delta_trs")) + { + const auto matrix = event["delta_trs"].get>(); + entry.imguizmo = makeScriptedMatrixFromArray(matrix); + } + else + { + const auto translation = event.contains("translation") ? event["translation"].get>() : std::array{0.f, 0.f, 0.f}; + const auto rotation = event.contains("rotation_deg") ? event["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; + const auto scale = event.contains("scale") ? event["scale"].get>() : std::array{1.f, 1.f, 1.f}; + entry.imguizmo = composeScriptedImguizmoTransform(translation, rotation, scale); + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +int32_t parseScriptedActionIntValue(const json_t& event) +{ + if (event.contains("value")) + return event["value"].get(); + if (event.contains("index")) + return event["index"].get(); + return 0; +} + +bool parseScriptedProjectionActionValue(const json_t& event, nbl::this_example::CCameraScriptedActionEvent& action, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (event.contains("value") && event["value"].is_string()) + { + const auto valueText = event["value"].get(); + if (valueText == "perspective") + action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Perspective); + else if (valueText == "orthographic") + action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Orthographic); + else + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueText + "\"."); + return false; + } + } + else + { + action.value = parseScriptedActionIntValue(event); + } + + return true; +} + +void parseScriptedActionEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("action")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); + return; + } + + const auto actionText = event["action"].get(); + nbl::this_example::CCameraScriptedActionEvent entry = { + .frame = frame + }; + + if (actionText == "set_active_render_window") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow); + entry.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_active_planar") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::SetActivePlanar); + entry.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_projection_type") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::SetProjectionType); + if (!parseScriptedProjectionActionValue(event, entry, out)) + return; + } + else if (actionText == "set_projection_index") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::SetProjectionIndex); + entry.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_use_window") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::SetUseWindow); + entry.value = event.value("value", false) ? 1 : 0; + } + else if (actionText == "set_left_handed") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::SetLeftHanded); + entry.value = event.value("value", false) ? 1 : 0; + } + else if (actionText == "reset_active_camera") + { + entry.code = nbl::this_example::CCameraScriptedActionUtilities::toCode(nbl::this_example::ECameraScriptedActionCode::ResetActiveCamera); + entry.value = 1; + } + else + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); + return; + } + + out.actionEvents.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedInputEventJson(const json_t& event, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("frame") || !event.contains("type")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event missing \"frame\" or \"type\"."); + return; + } + + const auto frame = event["frame"].get(); + const auto type = event["type"].get(); + const bool captureFrame = event.value("capture", false); + + if (type == "keyboard") + parseScriptedKeyboardEventJson(event, frame, captureFrame, out); + else if (type == "mouse") + parseScriptedMouseEventJson(event, frame, captureFrame, out); + else if (type == "imguizmo") + parseScriptedImguizmoEventJson(event, frame, captureFrame, out); + else if (type == "action") + parseScriptedActionEventJson(event, frame, captureFrame, out); + else + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); +} + +void parseScriptedInputEventsJson(const json_t& script, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("events")) + return; + + for (const auto& event : script["events"]) + parseScriptedInputEventJson(event, out); +} + +bool parseScriptedImguizmoVirtualCheckJson(const json_t& check, nbl::system::CCameraScriptedInputCheck& outCheck, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + outCheck.kind = nbl::system::CCameraScriptedInputCheck::Kind::ImguizmoVirtual; + outCheck.tolerance = check.value("tolerance", outCheck.tolerance); + + if (!check.contains("events")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); + return false; + } + + for (const auto& expectedEvent : check["events"]) + { + if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); + continue; + } + + const auto typeText = expectedEvent["type"].get(); + const auto type = nbl::core::CVirtualGimbalEvent::stringToVirtualEvent(typeText); + if (type == nbl::core::CVirtualGimbalEvent::None) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeText + "\"."); + continue; + } + + nbl::system::CCameraScriptedInputCheck::ExpectedVirtualEvent expected; + expected.type = type; + expected.magnitude = expectedEvent["magnitude"].get(); + outCheck.expectedVirtualEvents.emplace_back(expected); + } + + return true; +} + +bool parseScriptedCheckJson(const json_t& check, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!check.contains("frame") || !check.contains("kind")) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); + return false; + } + + const auto frame = check["frame"].get(); + const auto kind = check["kind"].get(); + + nbl::system::CCameraScriptedInputCheck entry; + entry.frame = frame; + + if (kind == "baseline") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::Baseline; + } + else if (kind == "imguizmo_virtual") + { + if (!parseScriptedImguizmoVirtualCheckJson(check, entry, out)) + return false; + } + else if (kind == "gimbal_near") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalNear; + entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); + + if (check.contains("position")) + { + readVector3(check["position"], entry.expectedPos); + entry.hasExpectedPos = true; + } + if (check.contains("euler_deg")) + { + readVector3(check["euler_deg"], entry.expectedEulerDeg); + entry.hasExpectedEuler = true; + } + } + else if (kind == "gimbal_delta") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalDelta; + entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); + } + else if (kind == "gimbal_step") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalStep; + + if (check.contains("min_pos_delta")) + { + entry.minPosDelta = check["min_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + if (check.contains("max_pos_delta")) + { + entry.posTolerance = check["max_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + else if (check.contains("pos_tolerance")) + { + entry.posTolerance = check["pos_tolerance"].get(); + entry.hasPosDeltaConstraint = true; + } + + if (check.contains("min_euler_delta_deg")) + { + entry.minEulerDeltaDeg = check["min_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + if (check.contains("max_euler_delta_deg")) + { + entry.eulerToleranceDeg = check["max_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + else if (check.contains("euler_tolerance_deg")) + { + entry.eulerToleranceDeg = check["euler_tolerance_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + + if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); + return false; + } + } + else + { + nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); + return false; + } + + out.timeline.checks.emplace_back(std::move(entry)); + return true; +} + +void parseScriptedChecksJson(const json_t& script, nbl::this_example::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("checks")) + return; + + for (const auto& check : script["checks"]) + parseScriptedCheckJson(check, out); +} + +} // namespace impl + +bool CCameraScriptedRuntimePersistenceUtilities::readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error) +{ + json_t script; + try + { + script = json_t::parse(text); + } + catch (const json_t::exception& e) + { + if (error) + *error = e.what(); + return false; + } + + out = {}; + + if (script.contains("enabled")) + out.enabled = script["enabled"].get(); + if (script.contains("log")) + { + out.hasLog = true; + out.log = script["log"].get(); + } + if (script.contains("hard_fail")) + out.hardFail = script["hard_fail"].get(); + if (script.contains("visual_debug")) + out.visualDebug = script["visual_debug"].get(); + if (script.contains("visual_debug_target_fps")) + out.visualTargetFps = script["visual_debug_target_fps"].get(); + if (script.contains("visual_debug_hold_seconds")) + out.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); + if (script.contains("enableActiveCameraMovement")) + { + out.hasEnableActiveCameraMovement = true; + out.enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); + } + if (script.contains("exclusive_input")) + out.exclusive = script["exclusive_input"].get() || out.exclusive; + if (script.contains("exclusive")) + out.exclusive = script["exclusive"].get() || out.exclusive; + if (script.contains("capture_prefix")) + out.capturePrefix = script["capture_prefix"].get(); + if (out.capturePrefix.empty()) + out.capturePrefix = "script"; + + impl::parseScriptedCaptureFramesJson(script, out); + + if (script.contains("camera_controls")) + impl::parseScriptedControlOverridesJson(script["camera_controls"], out.cameraControls); + + if (!impl::parseScriptedSequenceIfPresentJson(script, out, error)) + return false; + + impl::parseScriptedInputEventsJson(script, out); + impl::parseScriptedChecksJson(script, out); + + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(out.timeline); + nbl::this_example::CCameraScriptedActionUtilities::finalizeActionEvents(out.actionEvents); + return true; +} + +bool CCameraScriptedRuntimePersistenceUtilities::loadCameraScriptedInputFromFile(nbl::system::ISystem& system, const nbl::system::path& filePath, CCameraScriptedInputParseResult& out, std::string* error) +{ + std::string text; + if (!nbl::system::CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) + return false; + + return readCameraScriptedInput(text, out, error); +} + +} // namespace nbl::this_example diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 0e3248fdb..835e58471 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,6 +1,17 @@ -if(NBL_BUILD_IMGUI AND NBL_BUILD_FRUSTUM) - set(NBL_EXTRA_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/src/transform.cpp" +if(TARGET Nabla::ext::FullScreenTriangle AND TARGET Nabla::ext::Cameras AND TARGET Nabla::ext::ImGUI) + include("${NBL_ROOT_PATH}/cmake/nam/nam.cmake") + + file(GLOB_RECURSE NBL_EXTRA_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/*.inl" + "${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/*.inl" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.inl" ) set(NBL_INCLUDE_SERACH_DIRECTORIES @@ -8,14 +19,97 @@ if(NBL_BUILD_IMGUI AND NBL_BUILD_FRUSTUM) ) list(APPEND NBL_LIBRARIES - imtestengine - imguizmo - "${NBL_EXT_IMGUI_UI_LIB}" - "${NBL_EXT_FRUSTUM_LIB}" + argparse + Nabla::ext::ImGUI + Nabla::ext::FullScreenTriangle + Nabla::ext::Cameras ) - - # TODO; Arek I removed `NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET` from the last parameter here, doesn't this macro have 4 arguments anyway !? + nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") - # TODO: Arek temporarily disabled cause I haven't figured out how to make this target yet - # LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} nblExamplesGeometrySpirvBRD) -endif() \ No newline at end of file + + nam_add_channel_target( + TARGET ${EXECUTABLE_NAME}_envmaps + CHANNEL envmaps + MANIFEST_ROOT "${NBL_ROOT_PATH}/examples_tests/manifests" + REPO Devsh-Graphics-Programming/Nabla-Asset-Manifests + TAG envmaps + DESTINATION_ROOT "${NBL_ROOT_PATH}/examples_tests/runtime" + FLAT_RELEASE_ASSET_NAMES + ITEMS + space_spheremaps/LICENSE.txt + space_spheremaps/rich_blue_nebulae_1_8k.rgba16f.envblob + ) + add_dependencies(${EXECUTABLE_NAME} ${EXECUTABLE_NAME}_envmaps) + + set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") + file(GLOB_RECURSE NBL_HLSL_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/*.hlsl" + ) + target_sources(${EXECUTABLE_NAME} PRIVATE ${NBL_HLSL_SOURCES}) + set_source_files_properties(${NBL_HLSL_SOURCES} PROPERTIES HEADER_FILE_ONLY ON) + + set(SM 6_7) + + set(JSON [=[ +[ + { + "INPUT": "app_resources/imgui.unified.hlsl", + "KEY": "imgui.unified" + }, + { + "INPUT": "app_resources/sky_env_fragment.hlsl", + "KEY": "sky_env_fragment" + } +] +]=]) + string(CONFIGURE "${JSON}" JSON) + + set(COMPILE_OPTIONS + -isystem "${NBL_ROOT_PATH}/include" + -I "${CMAKE_CURRENT_SOURCE_DIR}" + -T lib_${SM} + ) + + NBL_CREATE_NSC_COMPILE_RULES( + TARGET ${EXECUTABLE_NAME}SPIRV + LINK_TO ${EXECUTABLE_NAME} + BINARY_DIR ${OUTPUT_DIRECTORY} + MOUNT_POINT_DEFINE NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT + COMMON_OPTIONS ${COMPILE_OPTIONS} + OUTPUT_VAR KEYS + INCLUDE nbl/this_example/builtin/build/spirv/keys.hpp + NAMESPACE nbl::this_example::builtin::build + INPUTS ${JSON} + ) + + NBL_CREATE_RESOURCE_ARCHIVE( + NAMESPACE nbl::this_example::builtin::build + TARGET ${EXECUTABLE_NAME}_builtinsBuild + LINK_TO ${EXECUTABLE_NAME} + BIND ${OUTPUT_DIRECTORY} + BUILTINS ${KEYS} + ) + + enable_testing() + + set(CAMERA_CONTINUITY_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameraz_continuity.json") + + add_test(NAME NBL_61_UI_CAMERA_SMOKE + COMMAND "$" --headless-camera-smoke --file "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameras.json" + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + add_test(NAME NBL_61_UI_CAMERA_CONTINUITY + COMMAND "$" --ci --script "${CAMERA_CONTINUITY_SCRIPT}" --script-visual-debug --no-screenshots + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + + set_tests_properties( + NBL_61_UI_CAMERA_SMOKE + NBL_61_UI_CAMERA_CONTINUITY + PROPERTIES + RUN_SERIAL TRUE + TIMEOUT 300 + ) +endif() diff --git a/61_UI/README.md b/61_UI/README.md index 6330f4673..acca847f2 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -1,2 +1,209 @@ -https://github.com/user-attachments/assets/6f779700-e6d4-4e11-95fb-7a7fddc47255 +# 61_UI Cameraz +`61_UI` is the full runnable integration and validation target for the shared camera stack documented in [`../../include/nbl/ext/Cameras/README.md`](../../include/nbl/ext/Cameras/README.md). + +If you want the reusable API design, start there first. +This README focuses on what `61_UI` adds on top of the shared layer. + +## Role of this example + +`61_UI` is used to: + +- exercise all current camera kinds in one visible scene +- validate the shared input, goal, preset, playback, follow, and scripted layers +- validate `referenceFrame` behavior across all camera kinds +- provide a manual playground for camera behavior +- provide CI-oriented smoke and continuity coverage + +It does not define camera semantics. +It consumes the shared camera API and exposes it through one concrete, testable app. + +## What `61_UI` owns locally + +The shared camera layer stops at reusable camera-domain APIs. +`61_UI` adds the local glue needed to turn that into an application: + +- scene setup and demo geometry +- planar / window routing +- explicit render-window selection for planar editing +- ImGui control panel +- transform editor and gizmo glue +- screenshot capture +- runtime logging and failure reporting +- local visual-debug presentation + +The shared layer owns: + +- camera semantics +- follow semantics +- compact sequence authoring +- scripted runtime payloads +- scripted check semantics + +`61_UI` owns how those pieces are presented, visualized, and driven in one sample. + +## Camera set + +`app_resources/cameras.json` configures the showcased cameras. +The current set is: + +- FPS +- Orbit +- Free +- Arcball +- Turntable +- TopDown +- Isometric +- Chase +- Dolly +- DollyZoom +- Path Rig + +These are exposed through the active planar / viewport configuration in the UI. + +## Follow target + +`61_UI` exposes one tracked target in the default scene. + +Tracked-target rule: + +- the reusable tracked subject is `core::CTrackedTarget` +- it owns its own gimbal +- it is not the large cone mesh +- the rendered marker is only a visualization of the tracked-target gimbal + +This matters because the shared follow layer is modeled around: + +- tracked-target pose +- follow mode +- follow config + +and not around a scene-node or mesh id. + +### Default follow usage + +The default scene uses: + +- `Free` + with `LookAtTarget` +- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path Rig` + with `OrbitTarget` +- `Chase`, `Dolly` + with `KeepLocalOffset` + +Manual runtime and scripted continuity both drive the same shared follow layer. + +## Scripted assets + +`61_UI` currently ships two camera-focused scripted assets: + +- `app_resources/cameraz_smoke_all.json` +- `app_resources/cameraz_continuity.json` + +### Smoke + +Purpose: + +- validate basic camera selection and movement +- validate `referenceFrame` application for every runtime camera kind +- validate shared helpers in a short, cheap run + +### Continuity + +Purpose: + +- validate smooth frame-to-frame motion +- validate follow lock while the tracked target moves +- validate typed restore and replay paths against the same shared camera semantics +- provide a readable visual-debug showcase + +The continuity asset is a compact authored camera-sequence script. +It is no longer a giant committed frame dump. + +## Shared pieces consumed directly by `61_UI` + +`61_UI` consumes the shared stack directly: + +- [`CCameraInputBindingUtilities.hpp`](../../include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp) +- [`CCameraPresetFlow.hpp`](../../include/nbl/ext/Cameras/CCameraPresetFlow.hpp) +- [`CCameraFollowUtilities.hpp`](../../include/nbl/ext/Cameras/CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](../../include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp) +- [`CCameraSequenceScript.hpp`](../../include/nbl/ext/Cameras/CCameraSequenceScript.hpp) +- [`CCameraScriptedRuntime.hpp`](../../include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp) +- [`CCameraScriptedRuntimePersistence.hpp`](include/camera/CCameraScriptedRuntimePersistence.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](include/camera/CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedCheckRunner.hpp`](../../include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp) + +`61_UI` does not define a private scripting model, private follow math, or private camera restore logic. + +## Reference-frame and gizmo validation + +`61_UI` is also the concrete harness for the shared `referenceFrame` seam used by ImGuizmo and other pose-driven tools. + +The current smoke coverage checks: + +- rigid reference application for `FPS` and `Free` +- legal-state projection from `referenceFrame` for all target-relative cameras +- typed `Path Rig` projection through the active path model +- restore back to baseline after reference-frame application + +`61_UI` is also the app used to exercise world-space and local-space gizmo semantics end-to-end against the shared camera API. + +## Local build and run + +Current local setup uses the Visual Studio 2022 dynamic preset. + +Configure: + +```powershell +cmake --preset user-configure-dynamic-msvc +``` + +Build: + +```powershell +cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m +``` + +Run tests: + +```powershell +ctest --test-dir build/dynamic/examples_tests/61_UI -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ +``` + +Run the example: + +```powershell +examples_tests/61_UI/bin/61_ui_d.exe +``` + +Run CI-style screenshot capture: + +```powershell +examples_tests/61_UI/bin/61_ui_d.exe --ci +``` + +Run smoke-style scripted playback: + +```powershell +examples_tests/61_UI/bin/61_ui_d.exe --script app_resources/cameraz_smoke_all.json --script-log +``` + +Run continuity with visual debug: + +```powershell +examples_tests/61_UI/bin/61_ui_d.exe --ci --script app_resources/cameraz_continuity.json --script-log --script-visual-debug +``` + +## Typical manual workflow + +1. Pick a camera and planar in the UI. +2. Drive the camera with keyboard, mouse, or ImGuizmo-backed controls. +3. Capture or restore presets if needed. +4. Move the tracked target marker. +5. Observe follow-enabled cameras and scripted overlays. + +## Summary + +`61_UI` is the app-layer harness around the shared camera API. +It proves that the reusable stack works end-to-end in a visible scene, with shared follow, presets, scripted playback, and CI validation all going through the same underlying camera semantics. diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json new file mode 100644 index 000000000..6e31e25f5 --- /dev/null +++ b/61_UI/app_resources/cameras.json @@ -0,0 +1,249 @@ +{ + "cameras": [ + { + "type": "FPS", + "position": [-2.438, 1.995, -3.130], + "orientation": [0.195, 0.311, -0.065, 0.928] + }, + { + "type": "Orbit", + "position": [9.000, 1.500, 0.000], + "target": [0, 0, 0] + }, + { + "type": "Free", + "position": [0.000, 1.500, -9.000], + "orientation": [0.082, 0.000, 0.000, 0.997] + }, + { + "type": "Arcball", + "position": [0.000, 1.500, 9.000], + "target": [0, 0, 0] + }, + { + "type": "Turntable", + "position": [-9.000, 1.500, 0.000], + "target": [0, 0, 0] + }, + { + "type": "TopDown", + "position": [0.0, 10.0, 0.0], + "target": [0, 0, 0] + }, + { + "type": "Isometric", + "position": [8.000, 4.000, 8.000], + "target": [0, 0, 0] + }, + { + "type": "Chase", + "position": [-12.000, 1.500, 12.000], + "target": [0, 0, 0] + }, + { + "type": "Dolly", + "position": [-12.000, 1.500, -12.000], + "target": [0, 0, 0] + }, + { + "type": "DollyZoom", + "position": [12.000, 1.500, -12.000], + "target": [0, 0, 0], + "baseFov": 40.0 + }, + { + "type": "PathRig", + "position": [12.000, 1.500, 12.000], + "target": [0, 0, 0] + } + ], + "projections": [ + { + "type": "perspective", + "fov": 40.0, + "zNear": 0.1, + "zFar": 110.0 + }, + { + "type": "orthographic", + "orthoWidth": 16.0, + "zNear": 0.1, + "zFar": 110.0 + } + ], + "viewports": [ + { + "projection": 0, + "bindings": { "keyboard": 0, "mouse": 0 } + }, + { + "projection": 1, + "bindings": { "keyboard": 0, "mouse": 0 } + }, + { + "projection": 0, + "bindings": { "keyboard": 3, "mouse": 1 } + }, + { + "projection": 1, + "bindings": { "keyboard": 3, "mouse": 1 } + }, + { + "projection": 0, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 1, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 1, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 1, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "bindings": { "keyboard": 0, "mouse": 1 } + }, + { + "projection": 1, + "bindings": { "keyboard": 0, "mouse": 1 } + }, + { + "projection": 0, + "bindings": { "keyboard": 1, "mouse": 1 } + }, + { + "projection": 1, + "bindings": { "keyboard": 1, "mouse": 1 } + }, + { + "projection": 0, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 1, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 1, + "bindings": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "bindings": { "keyboard": 3, "mouse": 1 } + }, + { + "projection": 1, + "bindings": { "keyboard": 3, "mouse": 1 } + }, + { + "projection": 0, + "bindings": { "keyboard": 2, "mouse": 1 } + }, + { + "projection": 1, + "bindings": { "keyboard": 2, "mouse": 1 } + } + ], + "planars": [ + { "camera": 0, "viewports": [0, 1] }, + { "camera": 1, "viewports": [2, 3] }, + { "camera": 2, "viewports": [4, 5] }, + { "camera": 3, "viewports": [6, 7] }, + { "camera": 4, "viewports": [8, 9] }, + { "camera": 5, "viewports": [10, 11] }, + { "camera": 6, "viewports": [12, 13] }, + { "camera": 7, "viewports": [14, 15] }, + { "camera": 8, "viewports": [16, 17] }, + { "camera": 9, "viewports": [18, 19] }, + { "camera": 10, "viewports": [20, 21] } + ], + "bindings": { + "keyboard": [ + { + "mappings": { + "W": "MoveForward", + "S": "MoveBackward", + "A": "MoveLeft", + "D": "MoveRight", + "I": "TiltDown", + "K": "TiltUp", + "J": "PanLeft", + "L": "PanRight" + } + }, + { + "mappings": { + "W": "MoveUp", + "S": "MoveDown", + "A": "MoveLeft", + "D": "MoveRight" + } + }, + { + "mappings": { + "W": "MoveForward", + "S": "MoveBackward", + "A": "MoveLeft", + "D": "MoveRight", + "E": "MoveUp", + "Q": "MoveDown", + "I": "TiltDown", + "K": "TiltUp", + "J": "PanLeft", + "L": "PanRight", + "U": "RollRight", + "O": "RollLeft" + } + }, + { + "mappings": { + "W": "MoveRight", + "S": "MoveLeft", + "A": "MoveDown", + "D": "MoveUp", + "E": "MoveForward", + "Q": "MoveBackward" + } + } + ], + "mouse": [ + { + "mappings": { + "RELATIVE_POSITIVE_MOVEMENT_X": "PanRight", + "RELATIVE_NEGATIVE_MOVEMENT_X": "PanLeft", + "RELATIVE_POSITIVE_MOVEMENT_Y": "TiltUp", + "RELATIVE_NEGATIVE_MOVEMENT_Y": "TiltDown" + } + }, + { + "mappings": { + "RELATIVE_POSITIVE_MOVEMENT_X": "MoveUp", + "RELATIVE_NEGATIVE_MOVEMENT_X": "MoveDown", + "RELATIVE_POSITIVE_MOVEMENT_Y": "MoveRight", + "RELATIVE_NEGATIVE_MOVEMENT_Y": "MoveLeft", + "VERTICAL_POSITIVE_SCROLL": "MoveForward", + "HORIZONTAL_POSITIVE_SCROLL": "MoveForward", + "VERTICAL_NEGATIVE_SCROLL": "MoveBackward", + "HORIZONTAL_NEGATIVE_SCROLL": "MoveBackward" + } + } + ] + } + } diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json new file mode 100644 index 000000000..f0448862a --- /dev/null +++ b/61_UI/app_resources/cameraz_continuity.json @@ -0,0 +1,586 @@ +{ + "enabled": true, + "log": false, + "exclusive": true, + "hard_fail": true, + "enableActiveCameraMovement": true, + "visual_debug": true, + "visual_debug_target_fps": 60, + "visual_debug_hold_seconds": 4, + "capture_prefix": "camera_continuity", + "fps": 60, + "defaults": { + "duration_seconds": 4.0, + "reset_camera": true, + "presentations": [ + { + "projection": "perspective", + "left_handed": true + }, + { + "projection": "orthographic", + "left_handed": true + } + ], + "continuity": { + "baseline": true, + "step": true, + "min_pos_delta": 0.00025, + "max_pos_delta": 2.0, + "min_euler_delta_deg": 0.00025, + "max_euler_delta_deg": 4.0 + }, + "captures": [ + "end" + ] + }, + "segments": [ + { + "name": "fps_sway", + "camera_kind": "FPS", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 0.9, + 0.35, + 1.6 + ], + "rotation_euler_deg_offset": [ + 7.5, + -22.0, + 3.5 + ] + } + } + ] + }, + { + "name": "orbit_swing", + "camera_kind": "Orbit", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 1.6, + -1.1, + 0.4 + ], + "orbit_u_delta_deg": -32.0, + "orbit_v_delta_deg": 10.0, + "orbit_distance_delta": -1.35 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 1.2, + -0.8, + 0.4 + ], + "rotation_euler_deg_offset": [ + 16.0, + 32.0, + -18.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 3.2, + -1.8, + 1.0 + ], + "rotation_euler_deg_offset": [ + 34.0, + 78.0, + -42.0 + ] + } + } + ] + }, + { + "name": "free_roll", + "camera_kind": "Free", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 0.7, + 0.8, + 1.8 + ], + "rotation_euler_deg_offset": [ + 5.0, + -16.0, + 12.0 + ] + } + } + ] + }, + { + "name": "arcball_orbit", + "camera_kind": "Arcball", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 1.1, + -1.4, + 0.5 + ], + "orbit_u_delta_deg": 72.0, + "orbit_v_delta_deg": -12.0, + "orbit_distance_delta": -0.7 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + -1.0, + 1.1, + 0.6 + ], + "rotation_euler_deg_offset": [ + 20.0, + 28.0, + 18.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + -2.5, + 2.7, + 1.4 + ], + "rotation_euler_deg_offset": [ + 42.0, + 68.0, + 44.0 + ] + } + } + ] + }, + { + "name": "turntable_push", + "camera_kind": "Turntable", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 1.8, + 0.9, + 0.3 + ], + "orbit_u_delta_deg": 92.0, + "orbit_distance_delta": -1.75 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 0.9, + 1.5, + 0.3 + ], + "rotation_euler_deg_offset": [ + 10.0, + 34.0, + -20.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 2.6, + 3.7, + 0.8 + ], + "rotation_euler_deg_offset": [ + 24.0, + 84.0, + -46.0 + ] + } + } + ] + }, + { + "name": "topdown_pan", + "camera_kind": "TopDown", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "target_offset": [ + 2.0, + -1.5, + 0.0 + ], + "orbit_u_delta_deg": 18.0, + "orbit_distance_delta": -1.0 + } + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 6.0, + -4.5, + 0.0 + ], + "orbit_u_delta_deg": 42.0, + "orbit_distance_delta": -3.0 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 1.8, + -1.2, + 1.0 + ], + "rotation_euler_deg_offset": [ + 30.0, + 22.0, + 18.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 4.8, + -3.4, + 2.1 + ], + "rotation_euler_deg_offset": [ + 68.0, + 58.0, + 38.0 + ] + } + } + ] + }, + { + "name": "isometric_pan", + "camera_kind": "Isometric", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 6.0, + 4.5, + 0.0 + ], + "orbit_distance_delta": -2.25 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 1.6, + 1.2, + 0.9 + ], + "rotation_euler_deg_offset": [ + 24.0, + -26.0, + 20.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 4.0, + 3.0, + 1.8 + ], + "rotation_euler_deg_offset": [ + 52.0, + -64.0, + 46.0 + ] + } + } + ] + }, + { + "name": "chase_arc", + "camera_kind": "Chase", + "keyframes": [ + { + "time": 0.0 + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 1.5, + -1.2, + 0.6 + ], + "rotation_euler_deg_offset": [ + 0.0, + 10.0, + 0.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 4.5, + -3.4, + 1.4 + ], + "rotation_euler_deg_offset": [ + 0.0, + 30.0, + 0.0 + ] + } + } + ] + }, + { + "name": "dolly_slide", + "camera_kind": "Dolly", + "keyframes": [ + { + "time": 0.0 + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 1.8, + 1.0, + 0.2 + ], + "rotation_euler_deg_offset": [ + 0.0, + -10.0, + 0.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 5.3, + 3.1, + -0.1 + ], + "rotation_euler_deg_offset": [ + 0.0, + -28.0, + 0.0 + ] + } + } + ] + }, + { + "name": "dollyzoom_breath", + "camera_kind": "DollyZoom", + "captures": [ + 0.25, + 0.5, + 0.75, + 1.0 + ], + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "orbit_distance_delta": -5.5, + "dynamic_base_fov_delta": 10.0, + "dynamic_reference_distance_delta": 2.5 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 0.8, + -0.5, + 0.4 + ], + "rotation_euler_deg_offset": [ + 12.0, + 30.0, + 16.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 2.4, + -1.4, + 1.0 + ], + "rotation_euler_deg_offset": [ + 26.0, + 74.0, + 38.0 + ] + } + } + ] + }, + { + "name": "path_arc", + "camera_kind": "PathRig", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "path_s_delta_deg": 42.0, + "path_u_delta": -3.5, + "path_v_delta": 1.0 + } + }, + { + "time": 4.0, + "delta": { + "path_s_delta_deg": 115.0, + "path_u_delta": -8.0, + "path_v_delta": 2.2 + } + } + ], + "target_keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "position_offset": [ + 1.4, + 0.8, + 0.8 + ], + "rotation_euler_deg_offset": [ + 18.0, + 38.0, + 22.0 + ] + } + }, + { + "time": 4.0, + "delta": { + "position_offset": [ + 4.0, + 2.5, + 1.9 + ], + "rotation_euler_deg_offset": [ + 42.0, + 102.0, + 48.0 + ] + } + } + ] + } + ] +} diff --git a/61_UI/app_resources/cameraz_smoke_all.json b/61_UI/app_resources/cameraz_smoke_all.json new file mode 100644 index 000000000..1e0fd4e20 --- /dev/null +++ b/61_UI/app_resources/cameraz_smoke_all.json @@ -0,0 +1,1209 @@ +{ + "enabled": true, + "log": true, + "exclusive": true, + "hard_fail": true, + "enableActiveCameraMovement": true, + "capture_prefix": "camera_smoke", + "capture_frames": [ + 64 + ], + "events": [ + { + "frame": 0, + "type": "action", + "action": "set_use_window", + "value": true + }, + { + "frame": 0, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 0, + "type": "action", + "action": "set_active_planar", + "value": 0 + }, + { + "frame": 0, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 0, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 0, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 0, + "type": "action", + "action": "set_active_planar", + "value": 0 + }, + { + "frame": 0, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 0, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 0, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1, + "type": "imguizmo", + "translation": [ + 4.0, + 2.12, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 3, + "type": "imguizmo", + "translation": [ + 4.030594, + 2.16456, + 1.03 + ], + "rotation_deg": [ + 0.0, + 8.0, + 5.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 6, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 6, + "type": "action", + "action": "set_active_planar", + "value": 1 + }, + { + "frame": 6, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 6, + "type": "action", + "action": "set_left_handed", + "value": false + }, + { + "frame": 6, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 6, + "type": "action", + "action": "set_active_planar", + "value": 1 + }, + { + "frame": 6, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 6, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 6, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 7, + "type": "imguizmo", + "translation": [ + 4.051537, + 2.103537, + 1.1 + ], + "rotation_deg": [ + 0.0, + 19.0, + 11.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 9, + "type": "imguizmo", + "translation": [ + 4.069324, + 2.153283, + 1.13 + ], + "rotation_deg": [ + 0.0, + 27.0, + 16.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 12, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 12, + "type": "action", + "action": "set_active_planar", + "value": 2 + }, + { + "frame": 12, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 12, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 12, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 12, + "type": "action", + "action": "set_active_planar", + "value": 2 + }, + { + "frame": 12, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 12, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 12, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 13, + "type": "imguizmo", + "translation": [ + 4.078836, + 2.058665, + 1.2 + ], + "rotation_deg": [ + 0.0, + 38.0, + 22.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 15, + "type": "imguizmo", + "translation": [ + 4.080867, + 2.106864, + 1.23 + ], + "rotation_deg": [ + 0.0, + 46.0, + 27.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 18, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 18, + "type": "action", + "action": "set_active_planar", + "value": 3 + }, + { + "frame": 18, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 18, + "type": "action", + "action": "set_left_handed", + "value": false + }, + { + "frame": 18, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 18, + "type": "action", + "action": "set_active_planar", + "value": 3 + }, + { + "frame": 18, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 18, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 18, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 19, + "type": "imguizmo", + "translation": [ + 4.069057, + 1.997696, + 1.3 + ], + "rotation_deg": [ + 0.0, + 57.0, + 33.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 21, + "type": "imguizmo", + "translation": [ + 4.054996, + 2.037824, + 1.33 + ], + "rotation_deg": [ + 0.0, + 65.0, + 38.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 24, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 24, + "type": "action", + "action": "set_active_planar", + "value": 4 + }, + { + "frame": 24, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 24, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 24, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 24, + "type": "action", + "action": "set_active_planar", + "value": 4 + }, + { + "frame": 24, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 24, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 24, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 25, + "type": "imguizmo", + "translation": [ + 4.026799, + 1.937359, + 1.4 + ], + "rotation_deg": [ + 0.0, + 76.0, + 44.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 27, + "type": "imguizmo", + "translation": [ + 3.998977, + 1.963986, + 1.43 + ], + "rotation_deg": [ + 0.0, + 84.0, + 49.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 30, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 30, + "type": "action", + "action": "set_active_planar", + "value": 5 + }, + { + "frame": 30, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 30, + "type": "action", + "action": "set_left_handed", + "value": false + }, + { + "frame": 30, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 30, + "type": "action", + "action": "set_active_planar", + "value": 5 + }, + { + "frame": 30, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 30, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 30, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 31, + "type": "imguizmo", + "translation": [ + 3.971937, + 1.89421, + 1.5 + ], + "rotation_deg": [ + 0.0, + 95.0, + 55.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 33, + "type": "imguizmo", + "translation": [ + 3.934965, + 1.903731, + 1.53 + ], + "rotation_deg": [ + 0.0, + 103.0, + 60.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 36, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 36, + "type": "action", + "action": "set_active_planar", + "value": 6 + }, + { + "frame": 36, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 36, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 36, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 36, + "type": "action", + "action": "set_active_planar", + "value": 6 + }, + { + "frame": 36, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 36, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 36, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 37, + "type": "imguizmo", + "translation": [ + 3.930274, + 1.880088, + 1.6 + ], + "rotation_deg": [ + 0.0, + 114.0, + 66.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 39, + "type": "imguizmo", + "translation": [ + 3.890281, + 1.871215, + 1.63 + ], + "rotation_deg": [ + 0.0, + 122.0, + 71.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 42, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 42, + "type": "action", + "action": "set_active_planar", + "value": 7 + }, + { + "frame": 42, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 42, + "type": "action", + "action": "set_left_handed", + "value": false + }, + { + "frame": 42, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 42, + "type": "action", + "action": "set_active_planar", + "value": 7 + }, + { + "frame": 42, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 42, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 42, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 43, + "type": "imguizmo", + "translation": [ + 3.921404, + 1.898869, + 1.7 + ], + "rotation_deg": [ + 0.0, + 133.0, + 77.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 45, + "type": "imguizmo", + "translation": [ + 3.885019, + 1.872802, + 1.73 + ], + "rotation_deg": [ + 0.0, + 141.0, + 82.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 48, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 48, + "type": "action", + "action": "set_active_planar", + "value": 8 + }, + { + "frame": 48, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 48, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 48, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 48, + "type": "action", + "action": "set_active_planar", + "value": 8 + }, + { + "frame": 48, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 48, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 48, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 49, + "type": "imguizmo", + "translation": [ + 3.949499, + 1.945398, + 1.8 + ], + "rotation_deg": [ + 0.0, + 152.0, + 88.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 51, + "type": "imguizmo", + "translation": [ + 3.922753, + 1.905666, + 1.83 + ], + "rotation_deg": [ + 0.0, + 160.0, + 93.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 54, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 54, + "type": "action", + "action": "set_active_planar", + "value": 9 + }, + { + "frame": 54, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 54, + "type": "action", + "action": "set_left_handed", + "value": false + }, + { + "frame": 54, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 54, + "type": "action", + "action": "set_active_planar", + "value": 9 + }, + { + "frame": 54, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 54, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 54, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 55, + "type": "imguizmo", + "translation": [ + 4.001345, + 2.006909, + 1.9 + ], + "rotation_deg": [ + 0.0, + 171.0, + 99.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 57, + "type": "imguizmo", + "translation": [ + 3.988672, + 1.95889, + 1.93 + ], + "rotation_deg": [ + 0.0, + 179.0, + 104.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 60, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 60, + "type": "action", + "action": "set_active_planar", + "value": 10 + }, + { + "frame": 60, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 60, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 60, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 60, + "type": "action", + "action": "set_active_planar", + "value": 10 + }, + { + "frame": 60, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 60, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 60, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 61, + "type": "imguizmo", + "translation": [ + 4.052559, + 2.066525, + 2.0 + ], + "rotation_deg": [ + 0.0, + 190.0, + 110.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 63, + "type": "imguizmo", + "translation": [ + 4.056059, + 2.016717, + 2.03 + ], + "rotation_deg": [ + 0.0, + 198.0, + 115.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ], + "checks": [ + { + "frame": 0, + "kind": "baseline" + }, + { + "frame": 1, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 3, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 6, + "kind": "baseline" + }, + { + "frame": 7, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 9, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 12, + "kind": "baseline" + }, + { + "frame": 13, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 15, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 18, + "kind": "baseline" + }, + { + "frame": 19, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 21, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 24, + "kind": "baseline" + }, + { + "frame": 25, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 27, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 30, + "kind": "baseline" + }, + { + "frame": 31, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 33, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 36, + "kind": "baseline" + }, + { + "frame": 37, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 39, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 42, + "kind": "baseline" + }, + { + "frame": 43, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 45, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 48, + "kind": "baseline" + }, + { + "frame": 49, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 51, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 54, + "kind": "baseline" + }, + { + "frame": 55, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 57, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 60, + "kind": "baseline" + }, + { + "frame": 61, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 63, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + } + ] +} \ No newline at end of file diff --git a/61_UI/app_resources/imgui.unified.hlsl b/61_UI/app_resources/imgui.unified.hlsl new file mode 100644 index 000000000..ed0e43cfb --- /dev/null +++ b/61_UI/app_resources/imgui.unified.hlsl @@ -0,0 +1,13 @@ +// Copyright (C) 2018-2026 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#define NBL_TEXTURES_BINDING_IX 0 +#define NBL_SAMPLER_STATES_BINDING_IX 1 +#define NBL_TEXTURES_SET_IX 0 +#define NBL_SAMPLER_STATES_SET_IX 0 +#define NBL_TEXTURES_COUNT 3 +#define NBL_SAMPLERS_COUNT 2 + +#include "nbl/ext/ImGui/builtin/hlsl/fragment.hlsl" +#include "nbl/ext/ImGui/builtin/hlsl/vertex.hlsl" diff --git a/61_UI/app_resources/imgui_fragment.hlsl b/61_UI/app_resources/imgui_fragment.hlsl new file mode 100644 index 000000000..5280ac2f8 --- /dev/null +++ b/61_UI/app_resources/imgui_fragment.hlsl @@ -0,0 +1,7 @@ +#define NBL_TEXTURES_BINDING_IX 0 +#define NBL_SAMPLER_STATES_BINDING_IX 1 +#define NBL_TEXTURES_SET_IX 0 +#define NBL_SAMPLER_STATES_SET_IX 0 +#define NBL_TEXTURES_COUNT 3 +#define NBL_SAMPLERS_COUNT 2 +#include "nbl/ext/ImGui/builtin/hlsl/fragment.hlsl" diff --git a/61_UI/app_resources/imgui_vertex.hlsl b/61_UI/app_resources/imgui_vertex.hlsl new file mode 100644 index 000000000..36257c853 --- /dev/null +++ b/61_UI/app_resources/imgui_vertex.hlsl @@ -0,0 +1 @@ +#include "nbl/ext/ImGui/builtin/hlsl/vertex.hlsl" diff --git a/61_UI/app_resources/sky_env_fragment.hlsl b/61_UI/app_resources/sky_env_fragment.hlsl new file mode 100644 index 000000000..1cd02d677 --- /dev/null +++ b/61_UI/app_resources/sky_env_fragment.hlsl @@ -0,0 +1,94 @@ +// Copyright (C) 2026-2026 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#pragma wave shader_stage(fragment) + +#include +using namespace nbl::hlsl; +using namespace ext::FullScreenTriangle; + +struct PushConstants +{ + float32_t4x4 invProj; + float32_t4x4 invViewRot; + uint32_t orthoMode; + uint32_t pad0; + uint32_t pad1; + uint32_t pad2; +}; + +[[vk::push_constant]] PushConstants pc; + +[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] Texture2D envMap; +[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] SamplerState envSampler; + +float32_t3 safeNormalize(float32_t3 v) +{ + const float32_t len2 = max(dot(v, v), 1e-12f); + return v * rsqrt(len2); +} + +float32_t3 safeHomogeneousDivide(float32_t4 v) +{ + float32_t w = v.w; + if (abs(w) < 1e-6f) + w = (w < 0.0f) ? -1e-6f : 1e-6f; + return v.xyz / w; +} + +float32_t3 acesToneMap(float32_t3 x) +{ + const float32_t a = 2.51f; + const float32_t b = 0.03f; + const float32_t c = 2.43f; + const float32_t d = 0.59f; + const float32_t e = 0.14f; + return saturate((x * (a * x + b)) / (x * (c * x + d) + e)); +} + +[[vk::location(0)]] float32_t4 main(SVertexAttributes vxAttr) : SV_Target0 +{ + const float32_t2 ndc = vxAttr.uv * 2.0f - float32_t2(1.0f, 1.0f); + float32_t3 dirVS; + if (pc.orthoMode != 0u) + { + const float32_t4 centerNearVS_H = mul(pc.invProj, float32_t4(0.0f, 0.0f, 0.0f, 1.0f)); + const float32_t4 centerFarVS_H = mul(pc.invProj, float32_t4(0.0f, 0.0f, 1.0f, 1.0f)); + const float32_t3 centerNearVS = safeHomogeneousDivide(centerNearVS_H); + const float32_t3 centerFarVS = safeHomogeneousDivide(centerFarVS_H); + const float32_t3 orthoForward = safeNormalize(centerFarVS - centerNearVS); + + const float32_t4 leftNearVS_H = mul(pc.invProj, float32_t4(-1.0f, 0.0f, 0.0f, 1.0f)); + const float32_t4 rightNearVS_H = mul(pc.invProj, float32_t4(1.0f, 0.0f, 0.0f, 1.0f)); + const float32_t4 downNearVS_H = mul(pc.invProj, float32_t4(0.0f, -1.0f, 0.0f, 1.0f)); + const float32_t4 upNearVS_H = mul(pc.invProj, float32_t4(0.0f, 1.0f, 0.0f, 1.0f)); + + const float32_t3 leftNearVS = safeHomogeneousDivide(leftNearVS_H); + const float32_t3 rightNearVS = safeHomogeneousDivide(rightNearVS_H); + const float32_t3 downNearVS = safeHomogeneousDivide(downNearVS_H); + const float32_t3 upNearVS = safeHomogeneousDivide(upNearVS_H); + + const float32_t3 orthoRight = safeNormalize(rightNearVS - leftNearVS); + const float32_t3 orthoUp = safeNormalize(upNearVS - downNearVS); + const float32_t tanHalfFov = 0.7673269879789604f; // tan(37.5 deg) + dirVS = safeNormalize(orthoForward + orthoRight * ndc.x * tanHalfFov + orthoUp * ndc.y * tanHalfFov); + } + else + { + const float32_t4 clip = float32_t4(ndc, 1.0f, 1.0f); + const float32_t4 viewH = mul(pc.invProj, clip); + dirVS = safeNormalize(safeHomogeneousDivide(viewH)); + } + const float32_t3 dir = safeNormalize(mul(pc.invViewRot, float32_t4(dirVS, 0.0f)).xyz); + + const float32_t invPi = 0.31830988618379067f; + const float32_t invTwoPi = 0.15915494309189535f; + float32_t2 envUv; + envUv.x = atan2(dir.z, dir.x) * invTwoPi + 0.5f; + envUv.y = acos(clamp(dir.y, -1.0f, 1.0f)) * invPi; + + float32_t3 color = max(envMap.SampleLevel(envSampler, envUv, 0.0f).rgb - 0.0010f, 0.0f); + color = acesToneMap(color * 0.45f); + return float32_t4(color, 1.0f); +} diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp new file mode 100644 index 000000000..8d1f467f6 --- /dev/null +++ b/61_UI/include/app/App.hpp @@ -0,0 +1,363 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_HPP_ +#define _NBL_THIS_EXAMPLE_APP_HPP_ + +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "argparse/argparse.hpp" + +#include "common.hpp" +#include "app/AppSwapchainResources.hpp" +#include "keysmapping.hpp" +#include "app/AppTypes.hpp" +#include "app/AppViewportBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCubeProjection.hpp" +#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" +#include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/this_example/builtin/build/spirv/keys.hpp" + +namespace nbl::system +{ + struct SCameraAppResourceContext; + struct SCameraConfigCollections; + struct SCameraPlanarRuntimeBootstrap; +} + +namespace nbl::this_example +{ + struct CCameraScriptedInputParseResult; +} + +class App final : public examples::SimpleWindowedApplication, public examples::BuiltinResourcesApplication +{ + using base_t = examples::SimpleWindowedApplication; + using asset_base_t = examples::BuiltinResourcesApplication; + using clock_t = std::chrono::steady_clock; + + struct SpaceEnvPushConstants + { + float32_t4x4 invProj = float32_t4x4(1.f); + float32_t4x4 invViewRot = float32_t4x4(1.f); + uint32_t orthoMode = 0u; + uint32_t pad0 = 0u; + uint32_t pad1 = 0u; + uint32_t pad2 = 0u; + }; + + public: + using base_t::base_t; + + inline App(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) + : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} + + // Will get called mid-initialization, via `filterDevices` between when the API Connection is created and Physical Device is chosen + core::vector getSurfaces() const override; + + bool onAppInitialized(smart_refctd_ptr&& system) override; + core::bitflag getLogLevelMask() override + { + return core::bitflag(nbl::system::ILogger::ELL_INFO) | + nbl::system::ILogger::ELL_WARNING | + nbl::system::ILogger::ELL_PERFORMANCE | + nbl::system::ILogger::ELL_ERROR; + } + + bool updateGUIDescriptorSet(); + + void workLoopBody() override; + + void paceScriptedVisualDebugFrame(); + + bool keepRunning() override; + bool onAppTerminated() override; + + void update(); + bool runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd_ptr&& system); + + private: + bool initializeMountedCameraResources(smart_refctd_ptr&& system); + nbl::hlsl::uint32_t2 getPresentationRenderExtent() const; + bool shouldMaximizePresentationWindow() const; + using CameraPreset = CCameraPreset; + using CameraKeyframe = CCameraKeyframe; + using CameraKeyframeTrack = CCameraKeyframeTrack; + + using PresetFilterMode = EPresetApplyPresentationFilter; + using PresetUiAnalysis = SCameraGoalApplyPresentation; + using CaptureUiAnalysis = SCameraCapturePresentation; + + using CameraConstraintSettings = SCameraConstraintSettings; + + ICamera* getActiveCamera(); + uint32_t getActivePlanarIx() const; + inline std::span> getPlanarProjectionSpan() + { + return { m_planarProjections.data(), m_planarProjections.size() }; + } + inline std::span> getPlanarProjectionSpan() const + { + return { m_planarProjections.data(), m_planarProjections.size() }; + } + nbl::system::SCameraAppResourceContext getCameraAppResourceContext() const; + SCameraFollowConfig* getActiveFollowConfig(); + const SCameraFollowConfig* getActiveFollowConfig() const; + SActiveViewportRuntimeState tryGetActiveViewportRuntimeState(); + bool tryBuildActiveCameraInputContext(SActiveCameraInputContext& outContext); + bool tryBuildActiveProjectionTabContext(SActiveProjectionTabContext& outContext); + bool tryBuildActiveScriptedCameraContext(SActiveScriptedCameraContext& outContext); + + uint32_t getManipulableObjectCount() const; + bool isManipulableObjectFollowTarget(uint32_t objectIx) const; + std::optional getManipulableObjectPlanarIx(uint32_t objectIx) const; + bool tryBuildManipulableObjectContext(uint32_t objectIx, SManipulableObjectContext& outContext) const; + bool tryBuildActiveManipulatedObjectContext(SManipulableObjectContext& outContext) const; + uint32_t getManipulatedObjectIx() const; + void bindManipulatedModel(); + void bindManipulatedFollowTarget(); + void bindManipulatedCamera(uint32_t planarIx); + void bindManipulatedObjectByIx(uint32_t objectIx); + void bindManipulableObject(const SManipulableObjectContext& context); + std::string getManipulableObjectLabel(uint32_t objectIx) const; + float32_t4x4 getManipulableObjectTransform(uint32_t objectIx) const; + float32_t3 getManipulableObjectWorldPosition(uint32_t objectIx) const; + float32_t3x4 computeFollowTargetMarkerWorld() const; + void applyManipulableObjectTransform(const SManipulableObjectContext& context, const float64_t4x4& transform); + + void setFollowTargetTransform(const float64_t4x4& transform); + + bool captureFollowOffsetsForPlanar(uint32_t planarIx); + bool followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const; + void refreshFollowOffsetConfigForPlanar(uint32_t planarIx); + void refreshFollowOffsetConfigsForCamera(ICamera* camera); + void refreshAllFollowOffsetConfigs(); + float64_t3 getDefaultFollowTargetPosition() const; + camera_quaternion_t getDefaultFollowTargetOrientation() const; + SCameraFollowConfig makeExampleDefaultFollowConfig(const ICamera* camera) const; + void resetFollowTargetToDefault(); + void snapFollowTargetToModel(); + void applyFollowToConfiguredCameras(bool allowDuringScriptedInput = false); + bool isOrbitLikeCamera(ICamera* camera); + void syncVisualDebugWindowBindings(); + void drawScriptVisualDebugOverlay(const ImVec2& displaySize); + + bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const; + PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const; + CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const; + CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const; + bool presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const; + CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset); + void storeApplyStatusBanner(ApplyStatusBanner& banner, std::string summary, bool succeeded, bool approximate); + void clearApplyStatusBanner(ApplyStatusBanner& banner); + void storePlaybackApplySummary(const SCameraPresetApplySummary& summary); + void appendVirtualEventLog(std::string_view source, std::string_view inputSource, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count); + SCameraPresetApplySummary applyPresetToTargets(const CameraPreset& preset); + bool tryBuildPlaybackPresetAtTime(float time, CameraPreset& preset); + bool applyPlaybackAtTime(float time); + void sortKeyframesByTime(); + void clampPlaybackTimeToKeyframes(); + int selectKeyframeNearestTime(float time); + void normalizeSelectedKeyframe(); + CameraKeyframe* getSelectedKeyframe(); + const CameraKeyframe* getSelectedKeyframe() const; + bool replaceSelectedKeyframeFromCamera(ICamera* camera); + void updatePlayback(double dtSec); + + bool savePresetsToFile(const nbl::system::path& path); + bool loadPresetsFromFile(const nbl::system::path& path); + bool saveKeyframesToFile(const nbl::system::path& path); + bool loadKeyframesFromFile(const nbl::system::path& path); + + void imguiListen(); + void drawWindowedViewportWindows(ImGuiIO& io, SImResourceInfo& info); + void drawWindowedViewportWindow(uint32_t windowIx, ImGuiCond windowCond, bool hideSceneGizmos, size_t& gizmoIx, SImResourceInfo& info); + void drawViewportWindowOverlay( + ImDrawList& drawList, + const nbl::ui::SViewportOverlayRect& viewportRect, + uint32_t windowIx, + const SWindowControlBinding& binding, + const nbl::ui::SBoundViewportCameraState& viewportState) const; + void updateActiveRenderWindowFromViewport(uint32_t windowIx, bool windowHovered, bool windowFocused); + void drawViewportManipulationGizmos( + uint32_t windowIx, + SWindowControlBinding& binding, + const nbl::ui::SBoundViewportCameraState& viewportState, + size_t& gizmoIx); + void drawManipulableObjectHoverOverlay(const SManipulableObjectContext& objectContext) const; + void drawViewportSplitOverlayWindow(const ImVec2& displaySize); + void drawFullscreenViewportWindow(ImGuiIO& io, SImResourceInfo& info); + void refreshViewportBindingMatrices(); + void finalizeUiFrameState(); + void updatePresentationTiming(); + SCapturedUiEvents captureUiInputEvents(); + void buildCameraInputEvents(const SCapturedUiEvents& capturedEvents, std::vector& outKeyboardEvents, std::vector& outMouseEvents) const; + nbl::ext::imgui::UI::SUpdateParameters buildUiUpdateParameters(const SCapturedUiEvents& capturedEvents) const; + void prepareScriptedFrameState(SAppFrameUpdateState::SPreparedScriptedFrame& outState); + void prepareCapturedCameraInput(const SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState, SAppFrameUpdateState::SPreparedCapturedInput& outCameraInput); + void prepareUiRuntimeState(const SAppFrameUpdateState::SPreparedCapturedInput& cameraInput, SAppFrameUpdateState::SUiRuntimeState& outUiState); + void prepareCameraAndUiInput(const SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState, SAppFrameUpdateState::SPreparedCapturedInput& outCameraInput, SAppFrameUpdateState::SUiRuntimeState& outUiState); + SAppFrameUpdateState buildFrameUpdateState(); + void runCameraFramePasses(SAppFrameUpdateState& frameState); + void applyPreparedCameraInput(const SAppFrameUpdateState::SPreparedCapturedInput& cameraInput, bool skipCameraInput); + void runPreparedScriptedFrame(SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState); + void updateUiFrame(const SAppFrameUpdateState::SUiRuntimeState& uiState); + void applyFrameRuntimeState(SAppFrameUpdateState& frameState); + bool initializeCameraConfiguration(const argparse::ArgumentParser& program); + bool tryBuildCameraConfigurationBootstrap( + const argparse::ArgumentParser& program, + nbl::system::SCameraPlanarRuntimeBootstrap& outRuntimeBootstrap, + std::optional& outPendingScriptedSequence); + bool initializePlanarRuntimeState(const nbl::system::SCameraPlanarRuntimeBootstrap& runtimeBootstrap, const std::optional& pendingScriptedSequence); + void initializePlanarFollowConfigs(); + bool tryLoadConfiguredScriptedInput(const argparse::ArgumentParser& program, const nbl::system::SCameraConfigCollections& cameraCollections, std::optional& outPendingScriptedSequence); + bool initializePresentationResources(); + bool initializeUiResources(); + bool initializeSceneResources(); + bool initializeGeometrySceneResources(); + bool initializeSceneRenderpass(); + bool initializeSpaceEnvironmentResources(); + bool initializeDebugSceneRendererResources(); + bool initializeWindowSceneFramebufferResources(); + uint32_t getFramesInFlight() const; + bool waitForInflightFrameSlot(); + std::optional tryBuildFrameSubmissionContext(); + bool recordFramePasses(const SFrameSubmissionContext& frameContext); + bool submitAndPresentFrame(const SFrameSubmissionContext& frameContext); + void resetScriptedInputRuntimeState(); + void finalizeScriptedInputRuntimeState(); + void applyParsedScriptedInput(nbl::this_example::CCameraScriptedInputParseResult parsed, std::optional& pendingScriptedSequence); + std::optional resolveSequenceSegmentPlanarIx(const CCameraSequenceSegment& segment) const; + bool expandPendingScriptedSequence(const CCameraSequenceScript& sequence); + void dequeueScriptedFrameInput(SScriptedFrameInputState& outFrame); + void applyScriptedFrameActions(std::span scriptedActions); + void ensureScriptedVisualPlanarState(); + void updateScriptedMouseButtons(std::span scriptedMouse); + void appendScriptedInputEvents(const SScriptedFrameInputState& scriptedFrame, SCapturedUiEvents& capturedEvents); + void syncDynamicPerspectiveForPlanar(planar_projection_t* planar, ICamera* camera); + void logScriptedVirtualEvents(const char* label, std::span events) const; + void applyActiveCameraInput(std::span keyboardEvents, std::span mouseEvents, bool skipCameraInput); + void applyScriptedImguizmoInput(SScriptedFrameInputState& scriptedFrame, bool skipCameraInput); + void applyScriptedGoals(const CCameraScriptedFrameEvents& scriptedFrameEvents, bool skipCameraInput); + void logScriptedCameraPose(const char* label, ICamera* camera) const; + void updateScriptedFollowVisualState(const CCameraScriptedFrameEvents& scriptedFrameEvents); + void runActiveFrameScriptedChecks(const SScriptedFrameInputState& scriptedFrame); + void updateSceneDebugInstances(); + void updateAuxSceneInstances(size_t geometryCount); + bool recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBinding& binding, uint32_t bindingIx); + bool recordUiRenderPass(IGPUCommandBuffer* cmdbuf, uint32_t resourceIx); + void captureRenderedFrame(IGPUImage* frame, uint64_t renderedFrameIx, const nbl::system::path& outPath, const char* tag); + void handleFrameCaptureRequests(IGPUImage* frame, uint64_t renderedFrameIx); + + bool shouldCaptureOSCursor(); + void UpdateBoundCameraMovement(); + void UpdateCursorVisibility(); + void UpdateUiMetrics(); + + void DrawControlPanel(); + void drawControlPanelTabs(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelHeader(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelToggles(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelGizmoTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + void drawControlPanelLogTab(const nbl::ui::SCameraControlPanelStyle& panelStyle); + + void TransformEditorContents(); + + void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true); + + std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); + clock_t::time_point start; + + /// @brief One window and surface. + smart_refctd_ptr> m_surface; + smart_refctd_ptr m_window; + // We can't use the same semaphore for acquire and present, because that would disable "Frames in Flight" by syncing previous present against next acquire. + // At least two timelines must be used. + smart_refctd_ptr m_semaphore; + // Maximum frames which can be simultaneously submitted, used to cycle through our per-frame resources like command buffers + constexpr static inline uint32_t MaxFramesInFlight = SCameraAppRuntimeDefaults::MaxFramesInFlight; + // Use a separate counter to cycle through our resources because `getAcquireCount()` increases upon spontaneous resizes with immediate blit-presents + uint64_t m_realFrameIx = 0; + // We'll write to the Triple Buffer with a Renderpass + core::smart_refctd_ptr m_renderpass = {}; + // These are atomic counters where the Surface lets us know what's the latest Blit timeline semaphore value which will be signalled on the resource + std::array m_blitWaitValues; + // Enough Command Buffers and other resources for all frames in flight! + std::array, MaxFramesInFlight> m_cmdBufs; + // Our own persistent images that don't get recreated with the swapchain + std::array, MaxFramesInFlight> m_tripleBuffers; + // Resources derived from the images + std::array, MaxFramesInFlight> m_framebuffers = {}; + // Input system for capturing system events + core::smart_refctd_ptr m_inputSystem; + // Handles mouse events + InputSystem::ChannelReader mouse; + // Handles keyboard events + InputSystem::ChannelReader keyboard; + /// @brief Next presentation timestamp. + std::chrono::microseconds m_nextPresentationTimestamp = {}; + + core::smart_refctd_ptr m_descriptorSetPool; + + struct CRenderUI + { + nbl::core::smart_refctd_ptr manager; + + struct + { + core::smart_refctd_ptr gui, scene; + } samplers; + + core::smart_refctd_ptr descriptorSet; + }; + + SCameraAppSceneInteractionState m_sceneInteraction; + + std::vector> m_planarProjections; + + void syncWindowInputBinding(SWindowControlBinding& binding); + void syncWindowInputBindingToProjection(SWindowControlBinding& binding); + + static constexpr inline auto MaxSceneFBOs = 2u; + SCameraAppViewportSessionState m_viewports; + + // UI font atlas + viewport FBO color attachment textures + constexpr static inline auto TotalUISampleTexturesAmount = 1u + MaxSceneFBOs; + + SCameraAppDebugSceneState m_debugScene; + SCameraAppSpaceEnvironmentState m_spaceEnvironment; + + CRenderUI m_ui; + video::CDumbPresentationOracle oracle; + + SCameraAppCliRuntimeState m_cliRuntime; + SScriptedInputRuntimeState m_scriptedInput; + CameraControlSettings m_cameraControls; + CameraConstraintSettings m_cameraConstraints; + core::smart_refctd_ptr m_logFormatter; + SCameraAppEventLogState m_eventLog; + SCameraAppPresetAuthoringState m_presetAuthoring; + SCameraAppPlaybackAuthoringState m_playbackAuthoring; + CCameraGoalSolver m_cameraGoalSolver; + SCameraAppPresentationTimingState m_presentationTiming; + SCameraAppUiMetricsState m_uiMetrics; + SCameraAppGizmoState m_gizmoState; +}; + + +#endif // _NBL_THIS_EXAMPLE_APP_HPP_ + diff --git a/61_UI/include/app/AppCameraConfigUtilities.hpp b/61_UI/include/app/AppCameraConfigUtilities.hpp new file mode 100644 index 000000000..91dd02986 --- /dev/null +++ b/61_UI/include/app/AppCameraConfigUtilities.hpp @@ -0,0 +1,106 @@ +#ifndef _APP_CAMERA_CONFIG_UTILITIES_HPP_ +#define _APP_CAMERA_CONFIG_UTILITIES_HPP_ + +#include +#include +#include +#include + +#include "app/AppResourceUtilities.hpp" +#include "app/AppTypes.hpp" + +namespace nbl::system +{ + +struct SCameraInputBindingCollections final +{ + std::vector keyboard; + std::vector mouse; +}; + +struct SCameraViewportBindingSelection final +{ + std::optional keyboard = std::nullopt; + std::optional mouse = std::nullopt; +}; + +struct SCameraViewportConfig final +{ + uint32_t projectionIx = 0u; + SCameraViewportBindingSelection bindings = {}; +}; + +struct SCameraPlanarConfig final +{ + uint32_t cameraIx = 0u; + std::vector viewportIxs = {}; +}; + +struct SCameraPlanarConfigCollections final +{ + std::vector viewports = {}; + std::vector planars = {}; + + inline bool valid() const + { + return !planars.empty(); + } +}; + +struct SCameraConfigCollections final +{ + std::string embeddedScriptedInputText = {}; + std::vector> cameras = {}; + std::vector projections = {}; + SCameraInputBindingCollections bindings = {}; + SCameraPlanarConfigCollections planarConfig = {}; + + inline bool hasEmbeddedScriptedInputText() const + { + return !embeddedScriptedInputText.empty(); + } +}; + +struct SCameraPlanarRuntimeBootstrap final +{ + SCameraConfigLoadResult loadResult = {}; + SCameraConfigCollections collections = {}; + std::vector> planars = {}; +}; + +bool tryLoadCameraConfigCollections( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraConfigLoadResult& outLoadResult, + SCameraConfigCollections& outCollections, + std::string* error = nullptr); + +bool tryBuildCameraConfigCollections( + const std::string_view text, + SCameraConfigCollections& outCollections, + std::string& error); + +bool tryBuildCameraPlanarRuntime( + const SCameraConfigCollections& collections, + std::vector>& outPlanars, + std::string& error); + +bool tryBuildCameraPlanarRuntimeBootstrap( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraPlanarRuntimeBootstrap& outBootstrap, + std::string* error = nullptr); + +bool tryGetEmbeddedCameraScriptedInputText( + const SCameraConfigCollections& collections, + std::string& outText); + +bool tryCaptureInitialPlanarPresets( + const core::CCameraGoalSolver& goalSolver, + std::span> planars, + std::vector& outPresets, + std::string& outError); + +} // namespace nbl::system + +#endif // _APP_CAMERA_CONFIG_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp new file mode 100644 index 000000000..55443d9cd --- /dev/null +++ b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp @@ -0,0 +1,86 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_CONTROL_PANEL_AUTHORING_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_CONTROL_PANEL_AUTHORING_UTILITIES_HPP_ + +#include +#include + +#include "camera/CCameraControlPanelUiUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPresentationUtilities.hpp" + +namespace nbl::ui +{ + +inline void drawApplyStatusBanner( + const std::string_view summary, + const bool succeeded, + const bool approximate, + const SCameraControlPanelStyle& panelStyle) +{ + if (summary.empty()) + return; + + const ImVec4 resultColor = succeeded ? (approximate ? panelStyle.WarnColor : panelStyle.GoodColor) : panelStyle.BadColor; + ImGui::TextColored(resultColor, "%.*s", static_cast(summary.size()), summary.data()); +} + +inline void drawGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation, const SCameraControlPanelStyle& panelStyle) +{ + if (presentation.badges.exact) + { + CCameraControlPanelUiUtilities::drawBadge("EXACT", panelStyle.GoodColor, panelStyle.BadgeTextColor, panelStyle); + } + else if (presentation.badges.bestEffort) + { + CCameraControlPanelUiUtilities::drawBadge("BEST-EFFORT", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); + } + + if (presentation.badges.dropsState) + { + ImGui::SameLine(); + CCameraControlPanelUiUtilities::drawBadge("DROPS STATE", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); + } + else if (presentation.badges.sharedStateOnly) + { + ImGui::SameLine(); + CCameraControlPanelUiUtilities::drawBadge("SHARED STATE", panelStyle.AccentColor, panelStyle.BadgeTextColor, panelStyle); + } + + if (presentation.badges.blocked) + { + ImGui::SameLine(); + CCameraControlPanelUiUtilities::drawBadge("BLOCKED", panelStyle.BadColor, panelStyle.BadgeTextColor, panelStyle); + } +} + +inline ImVec4 getGoalApplyPresentationColor(const SCameraGoalApplyPresentation& presentation, const SCameraControlPanelStyle& panelStyle) +{ + return !presentation.hasCamera ? panelStyle.BadColor : (presentation.exact() ? panelStyle.GoodColor : panelStyle.WarnColor); +} + +inline void drawGoalApplyPresentationSummary(const SCameraGoalApplyPresentation& presentation, const SCameraControlPanelStyle& panelStyle) +{ + const ImVec4 compatibilityColor = getGoalApplyPresentationColor(presentation, panelStyle); + + ImGui::TextDisabled("Source"); + ImGui::SameLine(); + ImGui::TextColored(panelStyle.MutedColor, "%s", presentation.sourceKindLabel.c_str()); + ImGui::TextDisabled("Goal state"); + ImGui::SameLine(); + ImGui::TextColored(panelStyle.MutedColor, "%s", presentation.goalStateLabel.c_str()); + ImGui::TextDisabled("Policy"); + ImGui::SameLine(); + ImGui::TextColored(presentation.canApply ? compatibilityColor : panelStyle.BadColor, "%s", presentation.policyLabel.c_str()); + ImGui::TextDisabled("Compatibility"); + ImGui::SameLine(); + ImGui::TextColored(compatibilityColor, "%s", presentation.compatibilityLabel.c_str()); + drawGoalApplyPresentationBadges(presentation, panelStyle); +} + +inline std::string buildKeyframeLabel(const size_t keyframeIx, const core::CCameraKeyframe& keyframe) +{ + return "[" + std::to_string(keyframeIx) + "] t=" + std::to_string(keyframe.time) + " " + keyframe.preset.name; +} + +} // namespace nbl::ui + +#endif // _NBL_THIS_EXAMPLE_APP_CONTROL_PANEL_AUTHORING_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppGizmoUtilities.hpp b/61_UI/include/app/AppGizmoUtilities.hpp new file mode 100644 index 000000000..e20b1f6c7 --- /dev/null +++ b/61_UI/include/app/AppGizmoUtilities.hpp @@ -0,0 +1,57 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_GIZMO_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_GIZMO_UTILITIES_HPP_ + +#include "app/AppTypes.hpp" +#include "app/AppViewportBindingUtilities.hpp" + +namespace nbl::ui +{ + +inline ImGuizmoModelM16InOut makeImGuizmoModel(const float32_t4x4& transform) +{ + return { + .inTRS = transform, + .outTRS = transform, + .outDeltaTRS = SCameraAppTransformEditorUiDefaults::IdentityTransform + }; +} + +inline hlsl::SRigidTransformComponents extractRigidTransformComponentsOrDefault(const float32_t4x4& transform) +{ + hlsl::SRigidTransformComponents components = {}; + if (hlsl::CCameraMathUtilities::tryExtractRigidTransformComponents(transform, components)) + return components; + + components.translation = float32_t3(transform[3].x, transform[3].y, transform[3].z); + components.orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + components.scale = SCameraAppTransformEditorUiDefaults::IdentityScale; + return components; +} + +inline float32_t4x4 composeRigidTransform( + const hlsl::float32_t3& translation, + const hlsl::float32_t3& eulerDegrees, + const hlsl::float32_t3& scale) +{ + return hlsl::CCameraMathUtilities::composeTransformMatrix( + translation, + hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegrees(eulerDegrees), + scale); +} + +inline float computeViewportGizmoClipSize( + const SBoundViewportCameraState& viewportState, + const float32_t3& worldPosition, + const float worldRadius) +{ + const auto viewPosition = mul(viewportState.viewMatrix, float32_t4(worldPosition, 1.0f)); + const float depth = std::max(SCameraAppViewportDefaults::MinPerspectiveGizmoDepth, hlsl::abs(viewPosition.z)); + if (viewportState.projection->getParameters().m_type == IPlanarProjection::CProjection::Perspective) + return (worldRadius * viewportState.projectionMatrix[1][1]) / depth; + + return worldRadius * viewportState.projectionMatrix[1][1]; +} + +} // namespace nbl::ui + +#endif // _NBL_THIS_EXAMPLE_APP_GIZMO_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp new file mode 100644 index 000000000..a6418bd0d --- /dev/null +++ b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp @@ -0,0 +1,348 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_PROJECTION_CONTROL_PANEL_UI_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_PROJECTION_CONTROL_PANEL_UI_UTILITIES_HPP_ + +#include +#include + +#include "app/AppViewportBindingUtilities.hpp" + +namespace nbl::ui +{ + +using camera_panel_slider_spec_t = SCameraControlPanelSliderSpec; + +template +inline bool drawRenderWindowSelector( + const size_t windowCount, + uint32_t& activeWindowIx, + RefreshRuntime&& refreshRuntime) +{ + if (windowCount == 0u) + return false; + + if (activeWindowIx >= windowCount) + activeWindowIx = 0u; + + int currentWindowIx = static_cast(activeWindowIx); + const auto currentWindowLabel = "Window " + std::to_string(currentWindowIx); + if (!ImGui::BeginCombo("Render Window", currentWindowLabel.c_str())) + return true; + + for (size_t windowIx = 0u; windowIx < windowCount; ++windowIx) + { + const bool isSelected = currentWindowIx == static_cast(windowIx); + const auto windowLabel = "Window " + std::to_string(windowIx); + if (ImGui::Selectable(windowLabel.c_str(), isSelected)) + { + currentWindowIx = static_cast(windowIx); + activeWindowIx = static_cast(currentWindowIx); + if (!refreshRuntime()) + { + ImGui::EndCombo(); + return false; + } + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + return true; +} + +template +inline bool drawProjectionPlanarSelector( + std::span> planarProjections, + SActiveProjectionTabContext& runtime, + RefreshRuntime&& refreshRuntime) +{ + auto& binding = runtime.requireBinding(); + int currentPlanarIx = static_cast(binding.activePlanarIx); + const auto currentPlanarLabel = "Planar " + std::to_string(currentPlanarIx); + if (!ImGui::BeginCombo("Active Planar", currentPlanarLabel.c_str())) + return true; + + for (size_t planarIx = 0u; planarIx < planarProjections.size(); ++planarIx) + { + const bool isSelected = currentPlanarIx == static_cast(planarIx); + const auto planarLabel = "Planar " + std::to_string(planarIx); + if (ImGui::Selectable(planarLabel.c_str(), isSelected)) + { + currentPlanarIx = static_cast(planarIx); + trySelectBindingPlanar( + planarProjections, + binding, + static_cast(currentPlanarIx)); + if (!refreshRuntime()) + { + ImGui::EndCombo(); + return false; + } + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + return true; +} + +inline std::string getProjectionPresetName( + const IPlanarProjection::CProjection::ProjectionType projectionType, + const uint32_t projectionIx) +{ + switch (projectionType) + { + case IPlanarProjection::CProjection::Perspective: + return "Perspective Projection Preset " + std::to_string(projectionIx); + case IPlanarProjection::CProjection::Orthographic: + return "Orthographic Projection Preset " + std::to_string(projectionIx); + default: + return "Unknown Projection Preset " + std::to_string(projectionIx); + } +} + +inline bool drawProjectionPresetSelector( + std::span> planarProjections, + SActiveProjectionTabContext& runtime, + const IPlanarProjection::CProjection::ProjectionType projectionType) +{ + bool updateBoundVirtualMaps = false; + auto& binding = runtime.requireBinding(); + auto& projections = runtime.requirePlanar().getPlanarProjections(); + if (!ImGui::BeginCombo("Projection Preset", getProjectionPresetName(projectionType, binding.boundProjectionIx.value()).c_str())) + return false; + + for (uint32_t projectionIx = 0u; projectionIx < projections.size(); ++projectionIx) + { + const auto& projection = projections[projectionIx]; + if (projection.getParameters().m_type != projectionType) + continue; + + const bool isSelected = projectionIx == binding.boundProjectionIx.value(); + if (ImGui::Selectable(getProjectionPresetName(projectionType, projectionIx).c_str(), isSelected)) + { + updateBoundVirtualMaps = trySelectBindingProjectionIndex( + planarProjections, + binding, + projectionIx); + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + return updateBoundVirtualMaps; +} + +template +inline bool drawProjectionTypeSelector( + std::span> planarProjections, + SActiveProjectionTabContext& runtime, + RefreshRuntime&& refreshRuntime) +{ + auto& binding = runtime.requireBinding(); + auto selectedProjectionType = runtime.requirePlanar().getPlanarProjections()[binding.boundProjectionIx.value()].getParameters().m_type; + constexpr const char* ProjectionTypeLabels[] = { "Perspective", "Orthographic" }; + int type = static_cast(selectedProjectionType); + if (ImGui::Combo("Projection Type", &type, ProjectionTypeLabels, IM_ARRAYSIZE(ProjectionTypeLabels))) + { + selectedProjectionType = static_cast(type); + trySelectBindingProjectionType( + planarProjections, + binding, + selectedProjectionType); + if (!refreshRuntime()) + return false; + } + + CCameraControlPanelUiUtilities::drawHoverHint("Switch projection type for this planar"); + return true; +} + +inline void drawProjectionHandednessControls(SWindowControlBinding& binding) +{ + if (ImGui::RadioButton("LH", binding.leftHandedProjection)) + binding.leftHandedProjection = true; + ImGui::SameLine(); + if (ImGui::RadioButton("RH", !binding.leftHandedProjection)) + binding.leftHandedProjection = false; + CCameraControlPanelUiUtilities::drawHoverHint("Toggle left or right handed projection"); +} + +inline void drawProjectionParameterControls( + SWindowControlBinding& binding, + IPlanarProjection::CProjection& boundProjection, + const bool useWindow) +{ + auto updateParameters = boundProjection.getParameters(); + if (useWindow) + CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Allow axes to flip##allowAxesToFlip", .value = &binding.allowGizmoAxesToFlip, .hint = "Allow ImGuizmo axes to flip based on view" }); + if (useWindow) + CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Draw debug grid##drawDebugGrid", .value = &binding.enableDebugGridDraw, .hint = "Toggle debug grid in the render window" }); + + drawProjectionHandednessControls(binding); + + updateParameters.m_zNear = std::clamp( + updateParameters.m_zNear, + SCameraAppProjectionUiDefaults::NearPlaneMin, + SCameraAppProjectionUiDefaults::NearPlaneMax); + updateParameters.m_zFar = std::clamp( + updateParameters.m_zFar, + SCameraAppProjectionUiDefaults::FarPlaneMin, + SCameraAppProjectionUiDefaults::FarPlaneMax); + for (const auto& spec : { + camera_panel_slider_spec_t{ .label = "zNear", .value = &updateParameters.m_zNear, .minValue = SCameraAppProjectionUiDefaults::NearPlaneMin, .maxValue = SCameraAppProjectionUiDefaults::NearPlaneMax, .format = "%.2f", .flags = ImGuiSliderFlags_Logarithmic, .hint = "Near clip plane" }, + camera_panel_slider_spec_t{ .label = "zFar", .value = &updateParameters.m_zFar, .minValue = SCameraAppProjectionUiDefaults::FarPlaneMin, .maxValue = SCameraAppProjectionUiDefaults::FarPlaneMax, .format = "%.1f", .flags = ImGuiSliderFlags_Logarithmic, .hint = "Far clip plane" } + }) + { + CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); + } + + switch (boundProjection.getParameters().m_type) + { + case IPlanarProjection::CProjection::Perspective: + CCameraControlPanelUiUtilities::drawSliderFloatWithHint({ + .label = "Fov", + .value = &updateParameters.m_planar.perspective.fov, + .minValue = SCameraAppProjectionUiDefaults::PerspectiveFovMinDeg, + .maxValue = SCameraAppProjectionUiDefaults::PerspectiveFovMaxDeg, + .format = "%.1f", + .flags = ImGuiSliderFlags_Logarithmic, + .hint = "Perspective field of view" + }); + boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); + break; + case IPlanarProjection::CProjection::Orthographic: + CCameraControlPanelUiUtilities::drawSliderFloatWithHint({ + .label = "Ortho width", + .value = &updateParameters.m_planar.orthographic.orthoWidth, + .minValue = SCameraAppProjectionUiDefaults::OrthoWidthMin, + .maxValue = SCameraAppProjectionUiDefaults::OrthoWidthMax, + .format = "%.1f", + .flags = ImGuiSliderFlags_Logarithmic, + .hint = "Orthographic width" + }); + boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); + break; + default: + break; + } +} + +inline void drawCursorBehaviourControls(bool& captureCursorInMoveMode, bool& resetCursorToCenter) +{ + if (!ImGui::TreeNodeEx("Cursor Behaviour")) + return; + + CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Capture OS cursor in move mode", .value = &captureCursorInMoveMode, .hint = "When disabled the app never warps or clamps system cursor" }); + if (captureCursorInMoveMode) + { + if (ImGui::RadioButton("Clamp to the window", !resetCursorToCenter)) + resetCursorToCenter = false; + if (ImGui::RadioButton("Reset to the window center", resetCursorToCenter)) + resetCursorToCenter = true; + } + else + { + ImGui::TextDisabled("Cursor lock disabled"); + } + + ImGui::TreePop(); +} + +inline void drawBoundCameraMotionControls(ICamera& camera) +{ + float moveSpeed = camera.getMoveSpeedScale(); + float rotationSpeed = camera.getRotationSpeedScale(); + ImGui::SliderFloat( + "Move speed factor", + &moveSpeed, + SCameraAppControlPanelRangeDefaults::MotionScaleMin, + SCameraAppControlPanelRangeDefaults::MotionScaleMax, + "%.4f", + ImGuiSliderFlags_Logarithmic); + CCameraControlPanelUiUtilities::drawHoverHint("Scale translation speed for this camera"); + if (camera.getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) + { + ImGui::SliderFloat( + "Rotate speed factor", + &rotationSpeed, + SCameraAppControlPanelRangeDefaults::MotionScaleMin, + SCameraAppControlPanelRangeDefaults::MotionScaleMax, + "%.4f", + ImGuiSliderFlags_Logarithmic); + } + CCameraControlPanelUiUtilities::drawHoverHint("Scale rotation speed for this camera"); + camera.setMotionScales(moveSpeed, rotationSpeed); +} + +template +inline void drawBoundCameraSection( + SActiveProjectionTabContext& runtime, + const uint32_t planarIx, + AddMatrixTable&& addMatrixTableFn, + SyncBinding&& syncBinding, + SyncBindingToProjection&& syncBindingToProjection) +{ + auto& binding = runtime.requireBinding(); + auto& camera = runtime.requireCamera(); + const auto flags = ImGuiTreeNodeFlags_DefaultOpen; + if (!ImGui::TreeNodeEx("Bound Camera", flags)) + return; + + ImGui::Text("Type: %s", camera.getIdentifier().data()); + ImGui::Text("Object Ix: %u", planarIx + SCameraAppSceneDefaults::CameraObjectIxOffset); + ImGui::Separator(); + + drawBoundCameraMotionControls(camera); + + ICamera::SphericalTargetState sphericalState; + if (camera.tryGetSphericalTargetState(sphericalState)) + { + float distance = sphericalState.distance; + const float uiMaxDistance = + std::isfinite(sphericalState.maxDistance) ? + sphericalState.maxDistance : + std::max(SCameraAppControlPanelRangeDefaults::ConstraintMaxDistanceMax, sphericalState.distance * 2.0f); + ImGui::SliderFloat( + "Distance", + &distance, + sphericalState.minDistance, + uiMaxDistance, + "%.4f", + ImGuiSliderFlags_Logarithmic); + CCameraControlPanelUiUtilities::drawHoverHint("Current orbit distance"); + camera.trySetSphericalDistance(distance); + } + + if (ImGui::TreeNodeEx("World Data", flags)) + { + auto& gimbal = camera.getGimbal(); + const auto position = hlsl::CCameraMathUtilities::castVector(gimbal.getPosition()); + const auto orientation = hlsl::CCameraMathUtilities::castVector(gimbal.getOrientation().data); + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + + addMatrixTableFn("Position", ("PositionTable_" + runtime.activePlanarIxString).c_str(), 1, 3, &position[0], false); + addMatrixTableFn("Orientation (Quaternion)", ("OrientationTable_" + runtime.activePlanarIxString).c_str(), 1, 4, &orientation[0], false); + addMatrixTableFn("View Matrix", ("ViewMatrixTable_" + runtime.activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) + { + syncBinding(binding); + if (displayKeyMappingsAndVirtualStatesInline(&binding.inputBinding)) + syncBindingToProjection(binding); + ImGui::TreePop(); + } + + ImGui::TreePop(); +} + +} // namespace nbl::ui + +#endif // _NBL_THIS_EXAMPLE_APP_PROJECTION_CONTROL_PANEL_UI_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppRenderPassUtilities.hpp b/61_UI/include/app/AppRenderPassUtilities.hpp new file mode 100644 index 000000000..925d1dccd --- /dev/null +++ b/61_UI/include/app/AppRenderPassUtilities.hpp @@ -0,0 +1,38 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_RENDER_PASS_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_RENDER_PASS_UTILITIES_HPP_ + +#include "common.hpp" +#include "app/AppTypes.hpp" + +inline asset::SViewport makeFramebufferViewport(const uint32_t width, const uint32_t height) +{ + asset::SViewport viewport = {}; + viewport.minDepth = SCameraAppFrameRuntimeDefaults::ViewportMinDepth; + viewport.maxDepth = SCameraAppFrameRuntimeDefaults::ViewportMaxDepth; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = width; + viewport.height = height; + return viewport; +} + +inline VkRect2D makeRenderArea(const uint32_t width, const uint32_t height) +{ + return { + .offset = { 0, 0 }, + .extent = { width, height } + }; +} + +inline float32_t4x4 buildInverseViewRotation(const float32_t3x4& viewMatrix) +{ + auto inverseViewRotation = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(viewMatrix)); + const auto xyzMask = SCameraAppFrameRuntimeDefaults::InverseViewRotationXyzMask; + inverseViewRotation[0] *= xyzMask; + inverseViewRotation[1] *= xyzMask; + inverseViewRotation[2] *= xyzMask; + inverseViewRotation[3] = SCameraAppFrameRuntimeDefaults::InverseViewRotationHomogeneousRow; + return inverseViewRotation; +} + +#endif // _NBL_THIS_EXAMPLE_APP_RENDER_PASS_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppResourcePathUtilities.hpp b/61_UI/include/app/AppResourcePathUtilities.hpp new file mode 100644 index 000000000..9971b6eb2 --- /dev/null +++ b/61_UI/include/app/AppResourcePathUtilities.hpp @@ -0,0 +1,227 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_RESOURCE_PATH_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_RESOURCE_PATH_UTILITIES_HPP_ + +#include +#include + +#include "app/AppResourceUtilities.hpp" +#include "nbl/system/ModuleLookupUtils.h" +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" + +namespace nbl::system +{ + +enum class EResourceLookupPolicy : uint8_t +{ + MountedOnly, + RequestedPath +}; + +struct SCameraTextResourceErrorPrefixes final +{ + static constexpr std::string_view CameraConfig = "Cannot open config"; + static constexpr std::string_view ScriptedInput = "Cannot open scripted input file"; + static constexpr std::string_view MissingContext = "Camera app resource context is not initialized."; +}; + +struct STextResourceLoadRequest final +{ + path pathValue = {}; + EResourceLookupPolicy lookupPolicy = EResourceLookupPolicy::RequestedPath; + std::string_view openErrorPrefix = {}; +}; + +template +struct SCameraAppResourcePathCandidates final +{ + std::array paths = {}; + size_t count = 0u; + + inline std::span asSpan() const + { + return { paths.data(), count }; + } + + inline bool appendUnique(const path& candidate) + { + for (size_t i = 0u; i < count; ++i) + { + if (paths[i] == candidate) + return true; + } + + if (count >= CandidateCount) + return false; + + paths[count++] = candidate; + return true; + } +}; + +inline path resolveInputPath(const path& localInputCWD, path pathValue) +{ + if (pathValue.is_relative()) + pathValue = (localInputCWD / pathValue).lexically_normal(); + return pathValue; +} + +inline bool isMountedAppResourcePath(const path& pathValue) +{ + if (pathValue.empty()) + return false; + + const auto begin = pathValue.begin(); + if (begin == pathValue.end()) + return false; + + return *begin == SCameraMountedResourcePaths::AppResourcesWorkingDirectory; +} + +inline path makeMountedAppResourcePath(const path& relativePath) +{ + if (relativePath.empty() || relativePath.is_absolute() || isMountedAppResourcePath(relativePath)) + return relativePath; + + return path(SCameraMountedResourcePaths::AppResourcesWorkingDirectory) / relativePath; +} + +template +inline SCameraAppResourcePathCandidates makeResourcePathCandidates( + const path& localInputCWD, + const path& pathValue, + const EResourceLookupPolicy lookupPolicy) +{ + SCameraAppResourcePathCandidates candidates = {}; + if (pathValue.empty()) + return candidates; + + if (pathValue.is_absolute()) + { + candidates.appendUnique(pathValue); + return candidates; + } + + if (lookupPolicy == EResourceLookupPolicy::MountedOnly || isMountedAppResourcePath(pathValue)) + { + candidates.appendUnique(makeMountedAppResourcePath(pathValue)); + return candidates; + } + + candidates.appendUnique(resolveInputPath(localInputCWD, pathValue)); + return candidates; +} + +template +inline bool loadFirstCandidatePath( + std::span candidates, + Loader&& loader, + path* outLoadedPath = nullptr) +{ + for (const auto& candidate : candidates) + { + if (!loader(candidate)) + continue; + + if (outLoadedPath) + *outLoadedPath = candidate; + return true; + } + return false; +} + +inline bool loadTextResource( + ISystem& system, + const path& localInputCWD, + const STextResourceLoadRequest& request, + std::string& outText, + path* outLoadedPath = nullptr, + std::string* error = nullptr) +{ + const auto candidates = makeResourcePathCandidates( + localInputCWD, + request.pathValue, + request.lookupPolicy); + if (loadFirstCandidatePath( + candidates.asSpan(), + [&](const path& candidate) -> bool + { + return CCameraFileUtilities::readTextFile(system, candidate, outText); + }, + outLoadedPath)) + { + return true; + } + + if (error) + *error = std::string(request.openErrorPrefix) + " \"" + request.pathValue.string() + "\"."; + return false; +} + +inline bool loadDefaultCameraConfigText( + ISystem& system, + const path& localInputCWD, + std::string& outText, + path* outLoadedPath = nullptr, + std::string* error = nullptr) +{ + return loadTextResource( + system, + localInputCWD, + { + .pathValue = path(SCameraConfigResourcePaths::DefaultCameraConfigRelativePath), + .lookupPolicy = EResourceLookupPolicy::MountedOnly, + .openErrorPrefix = SCameraTextResourceErrorPrefixes::CameraConfig + }, + outText, + outLoadedPath, + error); +} + +inline bool loadRequestedCameraConfigText( + ISystem& system, + const path& localInputCWD, + const path& requestedPath, + std::string& outText, + path* outLoadedPath = nullptr, + std::string* error = nullptr) +{ + return loadTextResource( + system, + localInputCWD, + { + .pathValue = requestedPath, + .lookupPolicy = EResourceLookupPolicy::RequestedPath, + .openErrorPrefix = SCameraTextResourceErrorPrefixes::CameraConfig + }, + outText, + outLoadedPath, + error); +} + +inline path getExamplesRuntimeDirectory() +{ + return (executableDirectory() / path(SCameraMountedResourcePaths::RuntimeRelativeDirectoryFromExecutable)).lexically_normal(); +} + +inline path getSharedEnvmapDirectory() +{ + return (getExamplesRuntimeDirectory() / path(SCameraMountedResourcePaths::SharedEnvmapChannelDirectory)).lexically_normal(); +} + +inline path getSpaceEnvBlobRelativePath() +{ + return path(SCameraEnvmapResourcePaths::SpaceEnvBlobDirectory) / path(SCameraEnvmapResourcePaths::SpaceEnvBlobCandidate); +} + +inline SCameraAppResourcePathCandidates makeSpaceEnvBlobCandidates() +{ + SCameraAppResourcePathCandidates candidates = {}; + const auto relativeBlobPath = getSpaceEnvBlobRelativePath(); + candidates.appendUnique(path(SCameraMountedResourcePaths::MountedSharedEnvmapWorkingDirectory) / relativeBlobPath); + candidates.appendUnique(getSharedEnvmapDirectory() / relativeBlobPath); + return candidates; +} + +} // namespace nbl::system + +#endif // _NBL_THIS_EXAMPLE_APP_RESOURCE_PATH_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppResourceUtilities.hpp b/61_UI/include/app/AppResourceUtilities.hpp new file mode 100644 index 000000000..812d3b8f0 --- /dev/null +++ b/61_UI/include/app/AppResourceUtilities.hpp @@ -0,0 +1,132 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ + +#include +#include +#include +#include +#include + +#include "common.hpp" + +namespace nbl::system +{ + +struct SSpaceEnvBlobHeader final +{ + uint32_t magic = 0u; + uint32_t width = 0u; + uint32_t height = 0u; + uint32_t format = 0u; + uint64_t payloadSize = 0ull; +}; + +struct SCameraMountedResourcePaths final +{ + static constexpr std::string_view AppResourcesWorkingDirectory = "app_resources"; + static constexpr std::string_view MountedSharedEnvmapWorkingDirectory = "app_resources/shared_envmap"; + static constexpr std::string_view RuntimeRelativeDirectoryFromExecutable = "../../runtime"; + static constexpr std::string_view SharedEnvmapChannelDirectory = "envmaps"; +}; + +struct SCameraConfigResourcePaths final +{ + static constexpr size_t CandidateCount = 2u; + static constexpr std::string_view DefaultCameraConfigRelativePath = "cameras.json"; +}; + +struct SCameraEnvmapResourcePaths final +{ + static constexpr uint32_t SpaceEnvBlobMagic = 0x31425645u; + static constexpr uint32_t SpaceEnvBlobFormatRgba16Sfloat = 2u; + static constexpr size_t CandidateCount = 2u; + static constexpr std::string_view SpaceEnvBlobDirectory = "space_spheremaps"; + static constexpr std::string_view SpaceEnvBlobCandidate = "rich_blue_nebulae_1_8k.rgba16f.envblob"; +}; + +struct SCameraAppResourceContext final +{ + ISystem* system = nullptr; + path localInputCWD = {}; + + inline explicit operator bool() const + { + return system != nullptr; + } +}; + +inline SCameraAppResourceContext makeCameraAppResourceContext(ISystem& system, const path& localInputCWD) +{ + return { + .system = &system, + .localInputCWD = localInputCWD + }; +} + +enum class ECameraConfigLoadSource : uint8_t +{ + RequestedPath, + DefaultConfig +}; + +struct SCameraConfigLoadRequest final +{ + std::optional requestedPath = std::nullopt; + bool fallbackToDefault = false; +}; + +struct SCameraConfigLoadResult final +{ + std::string text = {}; + path loadedPath = {}; + ECameraConfigLoadSource source = ECameraConfigLoadSource::DefaultConfig; + bool requestedPathLoadFailed = false; + std::string requestedPathError = {}; + + inline bool usedRequestedPath() const + { + return source == ECameraConfigLoadSource::RequestedPath; + } + + inline bool usedDefaultConfig() const + { + return source == ECameraConfigLoadSource::DefaultConfig; + } +}; + +struct SCameraScriptTextLoadResult final +{ + std::string text = {}; + path loadedPath = {}; +}; + +bool tryLoadCameraConfigText( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraConfigLoadResult& outResult, + std::string* error = nullptr); + +bool tryLoadCameraScriptText( + const SCameraAppResourceContext& context, + const path& scriptPath, + SCameraScriptTextLoadResult& outResult, + std::string* error = nullptr); + +bool mountOptionalSharedEnvmapResources( + const SCameraAppResourceContext& context, + ILogger* logger = nullptr); + +bool loadPreferredSpaceEnvBlob( + const SCameraAppResourceContext& context, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload, + path* outLoadedPath = nullptr); + +core::smart_refctd_ptr loadPrecompiledShaderFromAppResources( + asset::IAssetManager& assetManager, + ILogger* logger, + std::string_view key); + +} // namespace nbl::system + +#endif // _NBL_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppSwapchainResources.hpp b/61_UI/include/app/AppSwapchainResources.hpp new file mode 100644 index 000000000..663b7ea1b --- /dev/null +++ b/61_UI/include/app/AppSwapchainResources.hpp @@ -0,0 +1,123 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_SWAPCHAIN_RESOURCES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_SWAPCHAIN_RESOURCES_HPP_ + +#include "app/AppTypes.hpp" + +class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResources +{ +public: + constexpr static inline IQueue::FAMILY_FLAGS RequiredQueueFlags = IQueue::FAMILY_FLAGS::GRAPHICS_BIT; + + inline uint8_t getLastImageIndex() const + { + return m_lastImageIndex; + } + +protected: + inline core::bitflag getTripleBufferPresentStages() const override + { + return asset::PIPELINE_STAGE_FLAGS::BLIT_BIT; + } + + inline bool tripleBufferPresent( + IGPUCommandBuffer* cmdbuf, + const ISmoothResizeSurface::SPresentSource& source, + const uint8_t imageIndex, + const uint32_t qFamToAcquireSrcFrom) override + { + bool success = true; + auto acquiredImage = getImage(imageIndex); + m_lastImageIndex = imageIndex; + + const bool needToAcquireSrcOwnership = qFamToAcquireSrcFrom != IQueue::FamilyIgnored; + assert(!source.image->getCachedCreationParams().isConcurrentSharing() || !needToAcquireSrcOwnership); + + const auto blitDstLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL; + IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = {}; + + using image_barrier_t = decltype(depInfo.imgBarriers)::element_type; + const image_barrier_t preBarriers[2] = { + { + .barrier = { + .dep = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .srcAccessMask = asset::ACCESS_FLAGS::NONE, + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::BLIT_BIT, + .dstAccessMask = asset::ACCESS_FLAGS::TRANSFER_WRITE_BIT + } + }, + .image = acquiredImage, + .subresourceRange = { + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .oldLayout = IGPUImage::LAYOUT::UNDEFINED, + .newLayout = blitDstLayout + }, + { + .barrier = { + .dep = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .srcAccessMask = asset::ACCESS_FLAGS::NONE, + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::BLIT_BIT, + .dstAccessMask = asset::ACCESS_FLAGS::TRANSFER_READ_BIT + }, + .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::ACQUIRE, + .otherQueueFamilyIndex = qFamToAcquireSrcFrom + }, + .image = source.image, + .subresourceRange = TripleBufferUsedSubresourceRange + } + }; + + depInfo.imgBarriers = { preBarriers, needToAcquireSrcOwnership ? 2ull : 1ull }; + success &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); + + { + const auto srcOffset = source.rect.offset; + const auto srcExtent = source.rect.extent; + const auto dstExtent = acquiredImage->getCreationParameters().extent; + const IGPUCommandBuffer::SImageBlit regions[1] = { { + .srcMinCoord = { static_cast(srcOffset.x), static_cast(srcOffset.y), 0 }, + .srcMaxCoord = { srcExtent.width, srcExtent.height, 1 }, + .dstMinCoord = { 0, 0, 0 }, + .dstMaxCoord = { dstExtent.width, dstExtent.height, 1 }, + .layerCount = acquiredImage->getCreationParameters().arrayLayers, + .srcBaseLayer = 0, + .dstBaseLayer = 0, + .srcMipLevel = 0 + } }; + success &= cmdbuf->blitImage( + source.image, + IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL, + acquiredImage, + blitDstLayout, + regions, + IGPUSampler::ETF_LINEAR); + } + + const image_barrier_t postBarrier[1] = { + { + .barrier = { + .dep = preBarriers[0].barrier.dep.nextBarrier(asset::PIPELINE_STAGE_FLAGS::NONE, asset::ACCESS_FLAGS::NONE) + }, + .image = preBarriers[0].image, + .subresourceRange = preBarriers[0].subresourceRange, + .oldLayout = blitDstLayout, + .newLayout = IGPUImage::LAYOUT::PRESENT_SRC + } + }; + depInfo.imgBarriers = postBarrier; + success &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); + + return success; + } + +private: + uint8_t m_lastImageIndex = 0u; +}; + +#endif diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp new file mode 100644 index 000000000..0a586d4aa --- /dev/null +++ b/61_UI/include/app/AppTypes.hpp @@ -0,0 +1,720 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_TYPES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_TYPES_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" + +using planar_projections_range_t = std::vector; +using planar_projection_t = CPlanarProjection; + +struct ImGuizmoPlanarM16InOut +{ + float32_t4x4 view, projection; +}; + +struct ImGuizmoModelM16InOut +{ + float32_t4x4 inTRS, outTRS, outDeltaTRS; +}; + +struct SWindowControlBinding final +{ + static inline constexpr uint32_t InvalidPlanarIx = std::numeric_limits::max(); + + nbl::core::smart_refctd_ptr sceneFramebuffer; + nbl::core::smart_refctd_ptr sceneColorView; + nbl::core::smart_refctd_ptr sceneDepthView; + float32_t3x4 viewMatrix = float32_t3x4(1.f); + float32_t4x4 projectionMatrix = float32_t4x4(1.f); + float32_t4x4 viewProjMatrix = float32_t4x4(1.f); + + uint32_t activePlanarIx = 0u; + bool allowGizmoAxesToFlip = false; + bool enableDebugGridDraw = true; + bool isOrthographicProjection = false; + float aspectRatio = 16.f / 9.f; + bool leftHandedProjection = true; + CGimbalInputBinder inputBinding; + + std::optional boundProjectionIx = std::nullopt; + std::optional lastBoundPerspectivePresetProjectionIx = std::nullopt; + std::optional lastBoundOrthoPresetProjectionIx = std::nullopt; + std::optional inputBindingProjectionIx = std::nullopt; + uint32_t inputBindingPlanarIx = InvalidPlanarIx; + + inline void pickDefaultProjections(const planar_projections_range_t& projections) + { + auto init = [&](std::optional& presetix, IPlanarProjection::CProjection::ProjectionType requestedType) -> void + { + for (uint32_t i = 0u; i < projections.size(); ++i) + { + const auto& params = projections[i].getParameters(); + if (params.m_type == requestedType) + { + presetix = i; + break; + } + } + + assert(presetix.has_value()); + }; + + init(lastBoundPerspectivePresetProjectionIx = std::nullopt, IPlanarProjection::CProjection::Perspective); + init(lastBoundOrthoPresetProjectionIx = std::nullopt, IPlanarProjection::CProjection::Orthographic); + boundProjectionIx = lastBoundPerspectivePresetProjectionIx.value(); + inputBindingProjectionIx = std::nullopt; + inputBindingPlanarIx = InvalidPlanarIx; + } +}; + +struct SCameraAppSceneDefaults final +{ + static inline constexpr uint32_t ModelObjectIx = 0u; + static inline constexpr uint32_t FollowTargetObjectIx = 1u; + static inline constexpr uint32_t CameraObjectIxOffset = 2u; + static inline constexpr float FollowTargetMarkerScale = 0.28f; + static inline constexpr float FollowTargetMarkerScaleVisualDebug = 0.6f; + static inline const float64_t3 DefaultFollowTargetPosition = float64_t3(6.0, -4.5, 2.25); + static inline const camera_quaternion_t DefaultFollowTargetOrientation = CCameraMathUtilities::makeIdentityQuaternion(); +}; + +inline float32_t3x4 buildFollowTargetMarkerWorldTransform( + const CTrackedTarget& trackedTarget, + const float markerScale) +{ + const auto& targetGimbal = trackedTarget.getGimbal(); + const auto position = hlsl::CCameraMathUtilities::castVector(targetGimbal.getPosition()); + const auto orientation = hlsl::CCameraMathUtilities::castVector(targetGimbal.getOrientation().data); + const auto markerTransform = hlsl::CCameraMathUtilities::composeTransformMatrix( + position, + CCameraMathUtilities::makeQuaternionFromComponents(orientation.x, orientation.y, orientation.z, orientation.w), + float32_t3(markerScale, markerScale, markerScale)); + return float32_t3x4(hlsl::transpose(markerTransform)); +} + +struct SCameraAppViewportDefaults final +{ + static inline constexpr ImVec2 MinWindowSize = ImVec2(69.0f, 69.0f); + static inline constexpr ImVec2 MaxWindowSize = ImVec2(7680.0f, 4320.0f); + static inline constexpr float DefaultGizmoWorldRadius = 0.22f; + static inline constexpr float FollowTargetGizmoWorldRadius = 0.35f; + static inline constexpr float MinPerspectiveGizmoDepth = 0.001f; + static inline constexpr bool FlipGizmoY = true; + static inline constexpr float32_t2 WindowPaddingOffset = float32_t2(10.0f, 10.0f); +}; + +struct SCameraAppRuntimeDefaults final +{ + static inline constexpr uint32_t CiFramesBeforeCapture = 10u; + static inline constexpr auto CiMaxRuntime = std::chrono::minutes(2); + static inline constexpr auto DisplayImageDuration = std::chrono::milliseconds(900); + static inline constexpr size_t VirtualEventLogMax = 128u; + static inline constexpr size_t UiMetricSamples = 96u; + static inline constexpr uint32_t MaxFramesInFlight = 3u; +}; + +struct SCameraAppUiTextureSlots final +{ + static inline constexpr uint32_t FontAtlas = nbl::ext::imgui::UI::FontAtlasTexId; + static inline constexpr uint32_t FirstViewport = FontAtlas + 1u; + + static inline constexpr uint32_t viewport(const uint32_t windowIx) + { + return FirstViewport + windowIx; + } + + static inline SImResourceInfo makeDefaultViewportResourceInfo() + { + SImResourceInfo info = {}; + info.samplerIx = static_cast(nbl::ext::imgui::UI::DefaultSamplerIx::USER); + return info; + } +}; + +struct SCameraAppRenderDefaults final +{ + static inline constexpr auto SceneDepthFormat = EF_D32_SFLOAT; + static inline constexpr auto FinalSceneFormat = EF_R8G8B8A8_SRGB; + static inline constexpr IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = { 0.014f, 0.018f, 0.030f, 1.0f } }; + static inline constexpr IGPUCommandBuffer::SClearDepthStencilValue SceneClearDepth = { .depth = 0.0f }; +}; + +struct SCameraAppPresentationDefaults final +{ + static inline constexpr nbl::hlsl::uint32_t2 CiWindowExtent = nbl::hlsl::uint32_t2(1280u, 720u); + static inline constexpr nbl::hlsl::uint32_t2 WindowOrigin = nbl::hlsl::uint32_t2(32u, 32u); +}; + +struct SCameraAppFrameRuntimeDefaults final +{ + static inline constexpr float ViewportMinDepth = 1.0f; + static inline constexpr float ViewportMaxDepth = 0.0f; + static inline constexpr float32_t4 InverseViewRotationXyzMask = float32_t4(1.0f, 1.0f, 1.0f, 0.0f); + static inline constexpr float32_t4 InverseViewRotationHomogeneousRow = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + static inline constexpr IGPUCommandBuffer::SClearColorValue UiClearColor = { .float32 = { 0.0f, 0.0f, 0.0f, 1.0f } }; +}; + +struct SCameraAppSceneDebugDefaults final +{ + static inline constexpr float GridExtent = 32.0f; + static inline constexpr float GridVerticalOffset = -0.5f; +}; + +struct SCameraAppInputDefaults final +{ + static inline constexpr float KeyboardScale = 0.00625f; + static inline constexpr float UnitScale = 1.0f; +}; + +struct SCameraAppCameraFactoryDefaults final +{ + static inline constexpr double DefaultMoveScale = 0.01; + static inline constexpr double DefaultRotateScale = 0.003; + static inline constexpr double TargetRigMoveScale = 0.5; +}; + +struct SCameraAppProjectionUiDefaults final +{ + static inline constexpr float NearPlaneMin = 0.1f; + static inline constexpr float NearPlaneMax = 100.0f; + static inline constexpr float FarPlaneMin = 110.0f; + static inline constexpr float FarPlaneMax = 10000.0f; + static inline constexpr float PerspectiveFovMinDeg = 20.0f; + static inline constexpr float PerspectiveFovMaxDeg = 150.0f; + static inline constexpr float OrthoWidthMin = 1.0f; + static inline constexpr float OrthoWidthMax = 30.0f; +}; + +struct SCameraAppControlPanelRangeDefaults final +{ + static inline constexpr float MotionScaleMin = 0.0001f; + static inline constexpr float MotionScaleMax = 10.0f; + static inline constexpr float InputScaleMin = 0.01f; + static inline constexpr float InputScaleMax = 10.0f; + static inline constexpr float ConstraintDistanceMin = 0.01f; + static inline constexpr float ConstraintMinDistanceMax = 1000.0f; + static inline constexpr float ConstraintMaxDistanceMax = 10000.0f; + static inline constexpr float ConstraintAngleMinDeg = -180.0f; + static inline constexpr float ConstraintAngleMaxDeg = 180.0f; +}; + +struct SCameraAppViewportLayoutDefaults final +{ + static inline constexpr float ControlPanelWidthRatio = 0.33f; + static inline constexpr float ControlPanelMinWidth = 380.0f; + static inline constexpr float ControlPanelMaxWidthRatio = 0.48f; + static inline constexpr float RenderPaddingX = 0.0f; + static inline constexpr float RenderPaddingY = 0.0f; + static inline constexpr float SplitGap = 4.0f; +}; + +struct SCameraAppScriptedVisualDefaults final +{ + static inline constexpr float TargetFps = 60.0f; + static inline constexpr float HoldSeconds = 3.0f; + static inline constexpr std::string_view DefaultCapturePrefix = "script"; +}; + +struct SCameraAppBindingEditorUiDefaults final +{ + static inline constexpr float TableColumnWeight = 0.33f; + static inline constexpr ImVec2 ActionButtonSize = ImVec2(100.0f, 30.0f); + static inline constexpr ImVec2 WindowInitialSize = ImVec2(600.0f, 400.0f); + static inline constexpr ImVec4 ActiveStatusColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); + static inline constexpr ImVec4 InactiveStatusColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); +}; + +struct SCameraAppTransformEditorUiDefaults final +{ + static inline constexpr ImVec4 GizmoActiveStatusColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); + static inline constexpr ImVec4 GizmoIdleStatusColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); + static inline constexpr float32_t4x4 IdentityTransform = float32_t4x4(1.0f); + static inline constexpr float32_t3 IdentityScale = float32_t3(1.0f); + static inline constexpr float32_t3 ZeroRotation = float32_t3(0.0f); +}; + +struct SCameraAppCliRuntimeState final +{ + bool ciMode = false; + bool ciScreenshotDone = false; + uint32_t ciFrameCounter = 0u; + nbl::system::path ciScreenshotPath; + std::chrono::steady_clock::time_point ciStartedAt = std::chrono::steady_clock::time_point::min(); + bool scriptVisualDebugCli = false; + bool disableScreenshotsCli = false; + bool headlessCameraSmokeMode = false; + bool headlessCameraSmokePassed = false; +}; + +struct SCameraAppUiMetricsState final +{ + std::array frameMs = {}; + std::array inputCounts = {}; + std::array virtualCounts = {}; + uint32_t sampleIndex = 0u; + uint32_t inputEventsThisFrame = 0u; + uint32_t virtualEventsThisFrame = 0u; + uint32_t lastInputEvents = 0u; + uint32_t lastVirtualEvents = 0u; + float lastFrameMs = 0.0f; +}; + +struct SCameraAppPresentationTimingState final +{ + std::chrono::microseconds lastPresentationTimestamp = {}; + bool hasLastPresentationTimestamp = false; + double frameDeltaSec = 0.0; +}; + +struct SCameraAppGizmoState final +{ + bool useSnap = false; + ImGuizmo::OPERATION operation = ImGuizmo::TRANSLATE; + ImGuizmo::MODE mode = ImGuizmo::LOCAL; + float snap[3] = { 1.0f, 1.0f, 1.0f }; +}; + +struct SScriptedVisualPlanarState final +{ + bool valid = false; + uint32_t planarIx = 0u; + uint64_t startFrame = 0u; + std::string segmentLabel; +}; + +struct SScriptedMouseButtonState final +{ + bool leftDown = false; + bool rightDown = false; +}; + +struct SScriptedFramePacerState final +{ + bool initialized = false; + std::chrono::steady_clock::time_point nextFrame = {}; +}; + +struct SScriptedInputRuntimeState final +{ + bool enabled = false; + bool log = false; + bool exclusive = false; + bool hardFail = false; + bool visualDebug = false; + float visualTargetFps = 0.f; + float visualCameraHoldSeconds = 0.f; + CCameraScriptedTimeline timeline = {}; + std::vector actionEvents = {}; + size_t nextEventIndex = 0u; + size_t nextActionIndex = 0u; + CCameraScriptedCheckRuntimeState checkRuntime = {}; + size_t nextCaptureIndex = 0u; + std::string capturePrefix = "script"; + nbl::system::path captureOutputDir; + bool failed = false; + bool summaryReported = false; + SScriptedVisualPlanarState visualPlanar = {}; + SCameraFollowVisualMetrics visualFollow = {}; + SScriptedMouseButtonState scriptedMouseButtons = {}; + SScriptedFramePacerState framePacer = {}; +}; + +struct SCapturedUiEvents final +{ + std::vector mouse; + std::vector keyboard; + + inline void clear() + { + mouse.clear(); + keyboard.clear(); + } + + inline uint32_t getEventCount() const + { + return static_cast(mouse.size() + keyboard.size()); + } +}; + +struct CUILogFormatter final : public nbl::system::ILogger +{ + CUILogFormatter() : ILogger(ILogger::DefaultLogMask()) {} + + std::string format(const E_LOG_LEVEL level, const std::string_view fmt, ...) + { + va_list args; + va_start(args, fmt); + auto out = constructLogString(fmt, level, args); + va_end(args); + if (!out.empty() && out.back() == '\n') + out.pop_back(); + return out; + } + +protected: + void log_impl(const std::string_view&, const E_LOG_LEVEL, va_list) override {} +}; + +struct VirtualEventLogEntry final +{ + uint64_t frame = 0u; + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; + float64_t magnitude = 0.0; + std::string source; + std::string inputSource; + std::string camera; + uint32_t planarIx = 0u; + std::string line; +}; + +struct CameraPlaybackState : CCameraPlaybackCursor +{ + bool overrideInput = true; +}; + +struct ApplyStatusBanner final +{ + std::string summary; + bool succeeded = false; + bool approximate = false; + + inline bool visible() const + { + return !summary.empty(); + } +}; + +struct SCameraAppAuthoringDefaults final +{ + static inline constexpr std::string_view DefaultPresetName = "Preset"; + static inline constexpr std::string_view DefaultPresetPath = "camera_presets.json"; + static inline constexpr std::string_view DefaultKeyframePath = "camera_keyframes.json"; + static inline constexpr int PresetListVisibleEntries = 6; + static inline constexpr size_t EventLogVisibleEntries = 200u; + static inline constexpr float PlaybackSpeedMin = 0.1f; + static inline constexpr float PlaybackSpeedMax = 4.0f; + static inline constexpr float KeyframeTimeStep = 0.1f; + static inline constexpr float KeyframeTimeFastStep = 1.0f; +}; + +struct SCameraAppEventLogState final +{ + std::deque entries = {}; + bool showHud = true; + bool showEventLog = false; + bool autoScroll = true; + bool wrap = true; +}; + +struct SCameraAppPresetAuthoringState final +{ + std::vector presets = {}; + std::vector initialPlanarPresets = {}; + ApplyStatusBanner applyBanner = {}; + nbl::ui::EPresetApplyPresentationFilter filterMode = nbl::ui::EPresetApplyPresentationFilter::All; + int selectedPresetIx = -1; + std::string presetName = std::string(SCameraAppAuthoringDefaults::DefaultPresetName); + std::string presetPath = std::string(SCameraAppAuthoringDefaults::DefaultPresetPath); +}; + +struct SCameraAppPlaybackAuthoringState final +{ + nbl::core::CCameraKeyframeTrack keyframeTrack = {}; + CameraPlaybackState playback = {}; + ApplyStatusBanner applyBanner = {}; + bool affectsAll = false; + float newKeyframeTime = 0.f; + std::string keyframePath = std::string(SCameraAppAuthoringDefaults::DefaultKeyframePath); +}; + +enum class SceneManipulatedObjectKind : uint8_t +{ + Model, + FollowTarget, + Camera +}; + +struct SManipulableObjectContext final +{ + uint32_t objectIx = SCameraAppSceneDefaults::ModelObjectIx; + SceneManipulatedObjectKind kind = SceneManipulatedObjectKind::Model; + std::optional planarIx = std::nullopt; + ICamera* camera = nullptr; + std::string label = "Model"; + float32_t4x4 transform = float32_t4x4(1.0f); + float32_t3 worldPosition = float32_t3(0.0f); + + inline bool isCamera() const + { + return kind == SceneManipulatedObjectKind::Camera && camera; + } + + inline bool isFollowTarget() const + { + return kind == SceneManipulatedObjectKind::FollowTarget; + } +}; + +struct SCameraAppSceneInteractionState final +{ + float32_t3x4 model = float32_t3x4(1.0f); + CTrackedTarget followTarget = {}; + std::vector planarFollowConfigs = {}; + bool followTargetVisible = true; + SceneManipulatedObjectKind manipulatedObjectKind = SceneManipulatedObjectKind::Model; + nbl::core::smart_refctd_ptr boundCameraToManipulate = nullptr; + std::optional boundPlanarCameraIxToManipulate = std::nullopt; +}; + +struct SCameraAppDebugSceneState final +{ + nbl::core::smart_refctd_ptr scene = {}; + nbl::core::smart_refctd_ptr renderpass = {}; + nbl::core::smart_refctd_ptr renderer = {}; + std::optional gridGeometryIx = std::nullopt; + std::optional followTargetGeometryIx = std::nullopt; + uint16_t geometrySelectionIx = 0u; +}; + +struct SCameraAppSpaceEnvironmentState final +{ + core::smart_refctd_ptr pipeline = {}; + core::smart_refctd_ptr descriptorSetLayout = {}; + core::smart_refctd_ptr descriptorPool = {}; + core::smart_refctd_ptr descriptorSet = {}; + core::smart_refctd_ptr image = {}; + core::smart_refctd_ptr imageView = {}; + core::smart_refctd_ptr sampler = {}; +}; + +struct CameraControlSettings final +{ + bool mirrorInput = false; + bool worldTranslate = false; + float keyboardScale = SCameraAppInputDefaults::KeyboardScale; + float mouseMoveScale = SCameraAppInputDefaults::UnitScale; + float mouseScrollScale = SCameraAppInputDefaults::UnitScale; + float translationScale = SCameraAppInputDefaults::UnitScale; + float rotationScale = SCameraAppInputDefaults::UnitScale; +}; + +struct SScriptedFrameInputState final +{ + CCameraScriptedFrameEvents frameEvents = {}; + std::vector actions = {}; + std::vector mouse; + std::vector keyboard; + std::vector imguizmoVirtualEvents; + + inline bool hasRuntimePayload() const + { + return !keyboard.empty() || + !mouse.empty() || + !actions.empty() || + !frameEvents.imguizmo.empty() || + !frameEvents.goals.empty() || + !frameEvents.trackedTargetTransforms.empty(); + } +}; + +struct SAppFrameUpdateState final +{ + struct SPreparedScriptedFrame final + { + bool skipCameraInput = false; + SScriptedFrameInputState frame = {}; + }; + + struct SPreparedCapturedInput final + { + SCapturedUiEvents capturedEvents = {}; + std::vector keyboardEvents = {}; + std::vector mouseEvents = {}; + }; + + struct SUiRuntimeState final + { + nbl::ext::imgui::UI::SUpdateParameters updateParams = {}; + }; + + SPreparedScriptedFrame scripted = {}; + SPreparedCapturedInput cameraInput = {}; + SUiRuntimeState ui = {}; +}; + +struct SFrameSubmissionContext final +{ + uint32_t resourceIx = 0u; + VkRect2D renderArea = {}; + IGPUImage* frame = nullptr; + IGPUCommandBuffer* cmdbuf = nullptr; + std::atomic_uint64_t* blitWaitValue = nullptr; +}; + +struct SViewportWindowFrame final +{ + ImVec2 contentRegionSize = {}; + ImVec2 cursorPos = {}; + nbl::ui::SViewportOverlayRect overlayRect = {}; + bool hovered = false; + bool focused = false; + bool mouseInside = false; +}; + +struct SImWindowInit final +{ + float32_t2 iPos = float32_t2(0.0f); + float32_t2 iSize = float32_t2(0.0f); +}; + +template +struct SAppWindowInitState final +{ + SImWindowInit trsEditor = {}; + SImWindowInit planars = {}; + std::array renderWindows = {}; +}; + +template +struct SCameraAppViewportSessionState final +{ + bool enableActiveCameraMovement = false; + bool captureCursorInMoveMode = false; + bool resetCursorToCenter = true; + bool useWindow = true; + uint32_t activeRenderWindowIx = 0u; + std::array windowBindings = {}; + SAppWindowInitState windowInit = {}; +}; + +struct SActiveViewportRuntimeState final +{ + SWindowControlBinding* binding = nullptr; + planar_projection_t* planar = nullptr; + ICamera* camera = nullptr; + + inline bool valid() const + { + return binding && planar && camera; + } + + inline SWindowControlBinding& requireBinding() const + { + assert(binding); + return *binding; + } + + inline planar_projection_t& requirePlanar() const + { + assert(planar); + return *planar; + } + + inline ICamera& requireCamera() const + { + assert(camera); + return *camera; + } +}; + +struct SActiveCameraInputContext final +{ + SActiveViewportRuntimeState viewport = {}; + + inline bool valid() const + { + return viewport.valid(); + } +}; + +struct SActiveProjectionTabContext final +{ + SActiveViewportRuntimeState viewport = {}; + std::string activeRenderWindowIxString = {}; + std::string activePlanarIxString = {}; + + inline bool valid() const + { + return viewport.valid(); + } + + inline SWindowControlBinding& requireBinding() const + { + return viewport.requireBinding(); + } + + inline planar_projection_t& requirePlanar() const + { + return viewport.requirePlanar(); + } + + inline ICamera& requireCamera() const + { + return viewport.requireCamera(); + } +}; + +struct SActiveScriptedCameraContext final +{ + SActiveViewportRuntimeState viewport = {}; + SCameraFollowConfig* followConfig = nullptr; + nbl::system::SCameraProjectionContext projectionContext = {}; + bool hasProjectionContext = false; + + inline bool valid() const + { + return viewport.valid(); + } + + inline SWindowControlBinding& requireBinding() const + { + return viewport.requireBinding(); + } + + inline planar_projection_t& requirePlanar() const + { + return viewport.requirePlanar(); + } + + inline ICamera& requireCamera() const + { + return viewport.requireCamera(); + } + + inline const nbl::system::SCameraProjectionContext* getProjectionContext() const + { + return hasProjectionContext ? &projectionContext : nullptr; + } +}; + +struct SActiveCameraInputTarget final +{ + ICamera* camera = nullptr; + uint32_t planarIx = SWindowControlBinding::InvalidPlanarIx; + + inline bool valid() const + { + return camera && planarIx != SWindowControlBinding::InvalidPlanarIx; + } +}; + +constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = +{ + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 +}; + +#endif // _NBL_THIS_EXAMPLE_APP_TYPES_HPP_ diff --git a/61_UI/include/app/AppViewportBindingUtilities.hpp b/61_UI/include/app/AppViewportBindingUtilities.hpp new file mode 100644 index 000000000..d2e27dfd9 --- /dev/null +++ b/61_UI/include/app/AppViewportBindingUtilities.hpp @@ -0,0 +1,303 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_VIEWPORT_BINDING_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_VIEWPORT_BINDING_UTILITIES_HPP_ + +#include +#include + +#include "app/AppTypes.hpp" +#include "nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp" + +namespace nbl::ui +{ + +struct SBoundViewportCameraState final +{ + ICamera* camera = nullptr; + IPlanarProjection::CProjection* projection = nullptr; + float32_t4x4 viewMatrix = float32_t4x4(1.0f); + float32_t4x4 projectionMatrix = float32_t4x4(1.0f); + float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); + ImGuizmoPlanarM16InOut imguizmoPlanar = {}; +}; + +inline bool tryBuildCameraQueryBinding( + std::span> planarProjections, + ICamera* camera, + SWindowControlBinding& outBinding) +{ + if (!camera) + return false; + + for (uint32_t planarIx = 0u; planarIx < planarProjections.size(); ++planarIx) + { + const auto& planar = planarProjections[planarIx]; + if (!planar || planar->getCamera() != camera) + continue; + + const auto& projections = planar->getPlanarProjections(); + if (projections.empty()) + return false; + + outBinding = {}; + outBinding.activePlanarIx = planarIx; + outBinding.aspectRatio = 1.0f; + outBinding.leftHandedProjection = true; + outBinding.boundProjectionIx = 0u; + + for (uint32_t ix = 0u; ix < projections.size(); ++ix) + { + if (projections[ix].getParameters().m_type != IPlanarProjection::CProjection::Perspective) + continue; + + outBinding.boundProjectionIx = ix; + break; + } + return true; + } + + return false; +} + +inline bool tryGetBindingPlanarProjections( + std::span> planarProjections, + const SWindowControlBinding& binding, + const planar_projections_range_t*& outProjections) +{ + outProjections = nullptr; + if (binding.activePlanarIx >= planarProjections.size()) + return false; + + const auto& planar = planarProjections[binding.activePlanarIx]; + if (!planar) + return false; + + outProjections = &planar->getPlanarProjections(); + return true; +} + +inline bool trySelectBindingPlanar( + std::span> planarProjections, + SWindowControlBinding& binding, + const uint32_t planarIx) +{ + if (planarIx >= planarProjections.size()) + return false; + + const auto& planar = planarProjections[planarIx]; + if (!planar) + return false; + + binding.activePlanarIx = planarIx; + binding.pickDefaultProjections(planar->getPlanarProjections()); + return true; +} + +inline bool ensureBindingDefaultProjections( + std::span> planarProjections, + SWindowControlBinding& binding) +{ + const planar_projections_range_t* projections = nullptr; + if (!tryGetBindingPlanarProjections(planarProjections, binding, projections)) + return false; + if (binding.lastBoundPerspectivePresetProjectionIx.has_value() && binding.lastBoundOrthoPresetProjectionIx.has_value()) + return true; + + binding.pickDefaultProjections(*projections); + return true; +} + +inline bool trySelectBindingProjectionType( + std::span> planarProjections, + SWindowControlBinding& binding, + const IPlanarProjection::CProjection::ProjectionType projectionType) +{ + if (!ensureBindingDefaultProjections(planarProjections, binding)) + return false; + + switch (projectionType) + { + case IPlanarProjection::CProjection::Perspective: + if (!binding.lastBoundPerspectivePresetProjectionIx.has_value()) + return false; + binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); + return true; + case IPlanarProjection::CProjection::Orthographic: + if (!binding.lastBoundOrthoPresetProjectionIx.has_value()) + return false; + binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); + return true; + default: + return false; + } +} + +inline bool trySelectBindingProjectionIndex( + std::span> planarProjections, + SWindowControlBinding& binding, + const uint32_t projectionIx) +{ + const planar_projections_range_t* projections = nullptr; + if (!tryGetBindingPlanarProjections(planarProjections, binding, projections)) + return false; + if (projectionIx >= projections->size()) + return false; + + binding.boundProjectionIx = projectionIx; + const auto projectionType = (*projections)[projectionIx].getParameters().m_type; + if (projectionType == IPlanarProjection::CProjection::Perspective) + binding.lastBoundPerspectivePresetProjectionIx = projectionIx; + else if (projectionType == IPlanarProjection::CProjection::Orthographic) + binding.lastBoundOrthoPresetProjectionIx = projectionIx; + return true; +} + +inline bool tryBuildWindowBindingMatrices( + std::span> planarProjections, + SWindowControlBinding& binding, + SBoundViewportCameraState& outState) +{ + if (!binding.boundProjectionIx.has_value()) + return false; + if (binding.activePlanarIx >= planarProjections.size()) + return false; + + auto& planar = planarProjections[binding.activePlanarIx]; + if (!planar) + return false; + + auto* const camera = planar->getCamera(); + if (!camera) + return false; + + auto& projections = planar->getPlanarProjections(); + const uint32_t projectionIx = binding.boundProjectionIx.value(); + if (projectionIx >= projections.size()) + return false; + + auto& projection = projections[projectionIx]; + nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(camera, projection); + projection.update(binding.leftHandedProjection, binding.aspectRatio); + + outState.camera = camera; + outState.projection = &projection; + outState.viewMatrix = getCastedMatrix(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(camera->getGimbal().getViewMatrix())); + outState.projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + outState.viewProjMatrix = mul(outState.projectionMatrix, outState.viewMatrix); + + binding.isOrthographicProjection = projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic; + binding.viewMatrix = getCastedMatrix(camera->getGimbal().getViewMatrix()); + binding.projectionMatrix = outState.projectionMatrix; + binding.viewProjMatrix = outState.viewProjMatrix; + return true; +} + +inline void buildProjectionContextFromViewportState( + const SBoundViewportCameraState& viewportState, + nbl::system::SCameraProjectionContext& outProjectionContext) +{ + outProjectionContext.viewMatrix = viewportState.viewMatrix; + outProjectionContext.projectionMatrix = viewportState.projectionMatrix; +} + +inline bool tryBuildActiveViewportRuntimeState( + std::span> planarProjections, + std::span windowBindings, + const uint32_t activeWindowIx, + SActiveViewportRuntimeState& outState) +{ + outState = {}; + if (activeWindowIx >= windowBindings.size()) + return false; + + auto& binding = windowBindings[activeWindowIx]; + if (binding.activePlanarIx >= planarProjections.size()) + return false; + + auto& planar = planarProjections[binding.activePlanarIx]; + auto* camera = planar ? planar->getCamera() : nullptr; + if (!planar || !camera) + return false; + + outState = { + .binding = &binding, + .planar = planar.get(), + .camera = camera + }; + return true; +} + +inline bool tryBuildBindingProjectionContext( + std::span> planarProjections, + SWindowControlBinding& binding, + nbl::system::SCameraProjectionContext& outProjectionContext) +{ + SBoundViewportCameraState viewportState = {}; + if (!tryBuildWindowBindingMatrices(planarProjections, binding, viewportState)) + return false; + + buildProjectionContextFromViewportState(viewportState, outProjectionContext); + return true; +} + +inline bool tryBuildViewportBoundCameraState( + std::span> planarProjections, + SWindowControlBinding& binding, + const ImVec2& viewportSize, + const bool flipGizmoY, + SBoundViewportCameraState& outState) +{ + constexpr float MinViewportExtent = std::numeric_limits::epsilon(); + if (viewportSize.x <= MinViewportExtent || viewportSize.y <= MinViewportExtent) + return false; + + binding.aspectRatio = viewportSize.x / viewportSize.y; + if (!tryBuildWindowBindingMatrices(planarProjections, binding, outState)) + return false; + + outState.imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(outState.viewMatrix)); + outState.imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(outState.projectionMatrix)); + if (flipGizmoY) + outState.imguizmoPlanar.projection[1][1] *= -1.0f; + return true; +} + +inline bool tryBuildCameraProjectionContext( + std::span> planarProjections, + ICamera* camera, + nbl::system::SCameraProjectionContext& outProjectionContext) +{ + SWindowControlBinding binding = {}; + if (!tryBuildCameraQueryBinding(planarProjections, camera, binding)) + return false; + + return tryBuildBindingProjectionContext(planarProjections, binding, outProjectionContext); +} + +inline bool initializeWindowBindingDefaults( + std::span> planarProjections, + std::span windowBindings) +{ + if (planarProjections.empty()) + return false; + + for (uint32_t windowIx = 0u; windowIx < windowBindings.size(); ++windowIx) + { + auto& binding = windowBindings[windowIx]; + binding.activePlanarIx = 0u; + + const auto& planar = planarProjections[binding.activePlanarIx]; + if (!planar) + return false; + + binding.pickDefaultProjections(planar->getPlanarProjections()); + binding.boundProjectionIx = windowIx == 0u ? + binding.lastBoundPerspectivePresetProjectionIx : + binding.lastBoundOrthoPresetProjectionIx; + } + + return true; +} + +} // namespace nbl::ui + +#endif // _NBL_THIS_EXAMPLE_APP_VIEWPORT_BINDING_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppViewportWindowUtilities.hpp b/61_UI/include/app/AppViewportWindowUtilities.hpp new file mode 100644 index 000000000..f1262fe5e --- /dev/null +++ b/61_UI/include/app/AppViewportWindowUtilities.hpp @@ -0,0 +1,102 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_VIEWPORT_WINDOW_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_VIEWPORT_WINDOW_UTILITIES_HPP_ + +#include "app/AppTypes.hpp" + +namespace nbl::ui +{ + +struct SViewportWindowRuntime final +{ + SViewportWindowFrame frame = {}; + SBoundViewportCameraState viewportState = {}; +}; + +inline SViewportWindowFrame buildViewportWindowFrame() +{ + SViewportWindowFrame frame = {}; + frame.contentRegionSize = ImGui::GetContentRegionAvail(); + frame.cursorPos = ImGui::GetCursorScreenPos(); + frame.overlayRect = { frame.cursorPos, frame.contentRegionSize }; + frame.hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + frame.focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows); + + const auto mousePos = ImGui::GetMousePos(); + frame.mouseInside = + mousePos.x >= frame.cursorPos.x && + mousePos.y >= frame.cursorPos.y && + mousePos.x <= frame.cursorPos.x + frame.contentRegionSize.x && + mousePos.y <= frame.cursorPos.y + frame.contentRegionSize.y; + return frame; +} + +inline bool tryBuildViewportWindowRuntime( + std::span> planarProjections, + SWindowControlBinding& binding, + const bool flipGizmoY, + SViewportWindowRuntime& outRuntime) +{ + outRuntime = {}; + outRuntime.frame = buildViewportWindowFrame(); + return tryBuildViewportBoundCameraState( + planarProjections, + binding, + outRuntime.frame.contentRegionSize, + flipGizmoY, + outRuntime.viewportState); +} + +inline void updateViewportWindowMoveFlag(ImGuiWindow* const window, const SViewportWindowFrame& frame) +{ + if (!window) + return; + + if (frame.mouseInside) + window->Flags |= ImGuiWindowFlags_NoMove; + else + window->Flags &= ~ImGuiWindowFlags_NoMove; +} + +inline void drawFollowTargetOverlayIfActive( + ImDrawList* const drawList, + const SBoundViewportCameraState& viewportState, + const nbl::core::CTrackedTarget& followTarget, + const SViewportOverlayRect& viewportRect, + const SScriptedInputRuntimeState& scriptedInput) +{ + if (!drawList) + return; + if (!(scriptedInput.enabled && scriptedInput.visualDebug && scriptedInput.visualFollow.active)) + return; + + CCameraViewportOverlayUtilities::drawFollowTargetViewportOverlay( + *drawList, + { + .viewMatrix = viewportState.viewMatrix, + .projectionMatrix = viewportState.projectionMatrix + }, + followTarget, + viewportRect); +} + +template +inline void drawViewportTextureAndOverlay( + SImResourceInfo& info, + const SViewportWindowRuntime& viewportRuntime, + const nbl::core::CTrackedTarget& followTarget, + const SScriptedInputRuntimeState& scriptedInput, + OverlayDrawFn&& drawOverlay) +{ + const auto& frame = viewportRuntime.frame; + ImGui::Image(info, frame.contentRegionSize); + ImGuizmo::SetRect(frame.cursorPos.x, frame.cursorPos.y, frame.contentRegionSize.x, frame.contentRegionSize.y); + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + { + drawOverlay(*drawList, frame.overlayRect, viewportRuntime.viewportState); + drawFollowTargetOverlayIfActive(drawList, viewportRuntime.viewportState, followTarget, frame.overlayRect, scriptedInput); + } +} + +} // namespace nbl::ui + +#endif // _NBL_THIS_EXAMPLE_APP_VIEWPORT_WINDOW_UTILITIES_HPP_ diff --git a/61_UI/include/camera/CCameraConstraintUtilities.hpp b/61_UI/include/camera/CCameraConstraintUtilities.hpp new file mode 100644 index 000000000..94d6e6349 --- /dev/null +++ b/61_UI/include/camera/CCameraConstraintUtilities.hpp @@ -0,0 +1,94 @@ +#ifndef _NBL_THIS_EXAMPLE_CAMERA_CONSTRAINT_UTILITIES_HPP_INCLUDED_ +#define _NBL_THIS_EXAMPLE_CAMERA_CONSTRAINT_UTILITIES_HPP_INCLUDED_ + +#include + +#include "nbl/ext/Cameras/CCameraGoalSolver.hpp" +#include "nbl/ext/Cameras/CCameraPresetFlow.hpp" + +namespace nbl::this_example +{ + +struct SCameraConstraintDefaults final +{ + static constexpr float PitchMinDeg = -80.0f; + static constexpr float PitchMaxDeg = 80.0f; + static constexpr float YawMinDeg = -180.0f; + static constexpr float YawMaxDeg = 180.0f; + static constexpr float RollMinDeg = -180.0f; + static constexpr float RollMaxDeg = 180.0f; + static constexpr float MinDistance = nbl::core::SCameraTargetRelativeTraits::MinDistance; + static constexpr float MaxDistance = nbl::core::SCameraTargetRelativeTraits::DefaultMaxDistance; +}; + +struct SCameraConstraintSettings +{ + bool enabled = false; + bool clampPitch = false; + bool clampYaw = false; + bool clampRoll = false; + bool clampDistance = false; + float pitchMinDeg = SCameraConstraintDefaults::PitchMinDeg; + float pitchMaxDeg = SCameraConstraintDefaults::PitchMaxDeg; + float yawMinDeg = SCameraConstraintDefaults::YawMinDeg; + float yawMaxDeg = SCameraConstraintDefaults::YawMaxDeg; + float rollMinDeg = SCameraConstraintDefaults::RollMinDeg; + float rollMaxDeg = SCameraConstraintDefaults::RollMaxDeg; + float minDistance = SCameraConstraintDefaults::MinDistance; + float maxDistance = SCameraConstraintDefaults::MaxDistance; +}; + +struct CCameraConstraintUtilities final +{ + static inline bool applyCameraConstraints( + const nbl::core::CCameraGoalSolver& solver, + nbl::core::ICamera* camera, + const SCameraConstraintSettings& constraints) + { + if (!constraints.enabled || !camera) + return false; + + if (camera->hasCapability(nbl::core::ICamera::SphericalTarget)) + { + if (!constraints.clampDistance) + return false; + + nbl::core::ICamera::SphericalTargetState sphericalState; + if (!camera->tryGetSphericalTargetState(sphericalState)) + return false; + + const float clamped = std::clamp(sphericalState.distance, constraints.minDistance, constraints.maxDistance); + if (clamped == sphericalState.distance) + return false; + + return camera->trySetSphericalDistance(clamped); + } + + if (!(constraints.clampPitch || constraints.clampYaw || constraints.clampRoll)) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto eulerDeg = nbl::hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + + auto clamped = eulerDeg; + if (constraints.clampPitch) + clamped.x = std::clamp(clamped.x, static_cast(constraints.pitchMinDeg), static_cast(constraints.pitchMaxDeg)); + if (constraints.clampYaw) + clamped.y = std::clamp(clamped.y, static_cast(constraints.yawMinDeg), static_cast(constraints.yawMaxDeg)); + if (constraints.clampRoll) + clamped.z = std::clamp(clamped.z, static_cast(constraints.rollMinDeg), static_cast(constraints.rollMaxDeg)); + + if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) + return false; + + nbl::core::CCameraPreset preset; + preset.goal.position = pos; + preset.goal.orientation = nbl::hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(clamped); + return nbl::core::CCameraPresetFlowUtilities::applyPreset(solver, camera, preset); + } +}; + +} // namespace nbl::this_example + +#endif diff --git a/61_UI/include/camera/CCameraScriptedActionUtilities.hpp b/61_UI/include/camera/CCameraScriptedActionUtilities.hpp new file mode 100644 index 000000000..450c98939 --- /dev/null +++ b/61_UI/include/camera/CCameraScriptedActionUtilities.hpp @@ -0,0 +1,82 @@ +#ifndef _NBL_THIS_EXAMPLE_CAMERA_SCRIPTED_ACTION_UTILITIES_HPP_INCLUDED_ +#define _NBL_THIS_EXAMPLE_CAMERA_SCRIPTED_ACTION_UTILITIES_HPP_INCLUDED_ + +#include +#include +#include + +#include "nbl/ext/Cameras/CCameraScriptedRuntime.hpp" + +namespace nbl::this_example +{ + +enum class ECameraScriptedActionCode : int32_t +{ + SetActiveRenderWindow = 1, + SetActivePlanar = 2, + SetProjectionType = 3, + SetProjectionIndex = 4, + SetUseWindow = 5, + SetLeftHanded = 6, + ResetActiveCamera = 7 +}; + +struct CCameraScriptedActionEvent final +{ + uint64_t frame = 0ull; + int32_t code = 0; + int32_t value = 0; +}; + +struct CCameraScriptedActionUtilities final +{ + static inline constexpr int32_t toCode(const ECameraScriptedActionCode code) + { + return static_cast(code); + } + + static inline bool hasCode(const CCameraScriptedActionEvent& action, const ECameraScriptedActionCode code) + { + return action.code == toCode(code); + } + + static inline void appendActionEvent( + std::vector& actions, + const uint64_t frame, + const ECameraScriptedActionCode code, + const int32_t value) + { + actions.emplace_back(CCameraScriptedActionEvent{ + .frame = frame, + .code = toCode(code), + .value = value + }); + } + + static inline void finalizeActionEvents(std::vector& actions) + { + std::stable_sort(actions.begin(), actions.end(), + [](const CCameraScriptedActionEvent& lhs, const CCameraScriptedActionEvent& rhs) + { + return lhs.frame < rhs.frame; + }); + } + + static inline void dequeueFrameActions( + const std::vector& actions, + size_t& nextActionIndex, + const uint64_t frame, + std::vector& out) + { + out.clear(); + while (nextActionIndex < actions.size() && actions[nextActionIndex].frame == frame) + { + out.emplace_back(actions[nextActionIndex]); + ++nextActionIndex; + } + } +}; + +} // namespace nbl::this_example + +#endif diff --git a/61_UI/include/camera/CCameraScriptedRuntimePersistence.hpp b/61_UI/include/camera/CCameraScriptedRuntimePersistence.hpp new file mode 100644 index 000000000..96dc3c4e4 --- /dev/null +++ b/61_UI/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -0,0 +1,64 @@ +#ifndef _NBL_THIS_EXAMPLE_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_INCLUDED_ +#define _NBL_THIS_EXAMPLE_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_INCLUDED_ + +#include +#include +#include +#include + +#include "camera/CCameraScriptedActionUtilities.hpp" +#include "nbl/ext/Cameras/CCameraScriptedRuntime.hpp" +#include "nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp" +#include "nbl/system/path.h" + +namespace nbl::this_example +{ + +struct CCameraScriptedControlOverrides +{ + bool hasKeyboardScale = false; + float keyboardScale = 1.f; + bool hasMouseMoveScale = false; + float mouseMoveScale = 1.f; + bool hasMouseScrollScale = false; + float mouseScrollScale = 1.f; + bool hasTranslationScale = false; + float translationScale = 1.f; + bool hasRotationScale = false; + float rotationScale = 1.f; +}; + +struct CCameraScriptedInputParseResult +{ + bool enabled = true; + bool hasLog = false; + bool log = false; + bool hardFail = false; + bool visualDebug = false; + float visualTargetFps = 0.f; + float visualCameraHoldSeconds = 0.f; + bool hasEnableActiveCameraMovement = false; + bool enableActiveCameraMovement = true; + bool exclusive = false; + std::string capturePrefix = "script"; + CCameraScriptedControlOverrides cameraControls = {}; + nbl::system::CCameraScriptedTimeline timeline = {}; + std::vector actionEvents = {}; + std::optional sequence; + std::vector warnings; +}; + +struct CCameraScriptedRuntimePersistenceUtilities final +{ + static inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out, std::string warning) + { + out.warnings.emplace_back(std::move(warning)); + } + + static bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error = nullptr); + static bool loadCameraScriptedInputFromFile(nbl::system::ISystem& system, const nbl::system::path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +}; + +} // namespace nbl::this_example + +#endif diff --git a/61_UI/include/camera/CCameraSequenceScriptedBuilder.hpp b/61_UI/include/camera/CCameraSequenceScriptedBuilder.hpp new file mode 100644 index 000000000..a8faab740 --- /dev/null +++ b/61_UI/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -0,0 +1,117 @@ +#ifndef _NBL_THIS_EXAMPLE_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_INCLUDED_ +#define _NBL_THIS_EXAMPLE_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_INCLUDED_ + +#include + +#include "camera/CCameraScriptedActionUtilities.hpp" +#include "nbl/ext/Cameras/CCameraScriptedRuntime.hpp" +#include "nbl/ext/Cameras/CCameraSequenceScript.hpp" +#include "nbl/ext/Cameras/ICamera.hpp" + +namespace nbl::this_example +{ + +struct CCameraSequenceScriptedSegmentBuildInfo +{ + uint32_t planarIx = 0u; + size_t availableWindowCount = 1u; + bool useWindow = false; + bool includeFollowTargetLock = false; +}; + +struct CCameraSequenceScriptedBuilderUtilities final +{ + static inline bool appendCompiledSequenceSegmentToScriptedTimeline( + nbl::system::CCameraScriptedTimeline& timeline, + std::vector& actionEvents, + const uint64_t baseFrame, + const nbl::core::CCameraSequenceCompiledSegment& compiledSegment, + const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, + std::string* error = nullptr) + { + std::vector framePolicies; + if (!nbl::core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) + { + if (error) + *error = "Failed to build compiled frame policies."; + return false; + } + + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetActiveRenderWindow, 0); + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetActivePlanar, static_cast(buildInfo.planarIx)); + if (!compiledSegment.presentations.empty()) + { + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); + } + if (compiledSegment.resetCamera) + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::ResetActiveCamera, 1); + + if (buildInfo.useWindow) + { + for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) + { + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetActiveRenderWindow, static_cast(windowIx)); + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetActivePlanar, static_cast(buildInfo.planarIx)); + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); + } + CCameraScriptedActionUtilities::appendActionEvent(actionEvents, baseFrame, ECameraScriptedActionCode::SetActiveRenderWindow, 0); + } + + for (const auto& policy : framePolicies) + { + nbl::core::CCameraPreset preset; + if (!nbl::core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) + { + if (error) + *error = "Failed to sample compiled segment track."; + return false; + } + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent( + timeline, + baseFrame + policy.frameOffset, + nbl::core::CCameraPresetUtilities::makeGoalFromPreset(preset)); + + if (compiledSegment.usesTrackedTargetTrack()) + { + nbl::core::CCameraSequenceTrackedTargetPose trackedTargetPose; + if (!nbl::core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) + { + if (error) + *error = "Failed to sample compiled tracked-target track."; + return false; + } + + nbl::core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); + } + + if (policy.baseline) + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); + if (policy.continuityStep) + { + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedGimbalStepCheck( + timeline, + baseFrame + policy.frameOffset, + compiledSegment.continuity.hasPosDeltaConstraint, + compiledSegment.continuity.maxPosDelta, + compiledSegment.continuity.minPosDelta, + compiledSegment.continuity.hasEulerDeltaConstraint, + compiledSegment.continuity.maxEulerDeltaDeg, + compiledSegment.continuity.minEulerDeltaDeg); + } + if (policy.followTargetLock) + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); + if (policy.capture) + timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); + } + + return true; + } +}; + +} // namespace nbl::this_example + +#endif diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index f79948e95..92cceeaf6 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -1,22 +1,224 @@ #ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ #define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ +#include #include "nbl/examples/examples.hpp" -// extensions -#include "nbl/ext/Frustum/CDrawFrustum.h" +// common api +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CFreeLockCamera.hpp" +#include "nbl/ext/Cameras/CSphericalTargetCamera.hpp" +#include "nbl/ext/Cameras/COrbitCamera.hpp" +#include "nbl/ext/Cameras/CArcballCamera.hpp" +#include "nbl/ext/Cameras/CTurntableCamera.hpp" +#include "nbl/ext/Cameras/CTopDownCamera.hpp" +#include "nbl/ext/Cameras/CIsometricCamera.hpp" +#include "nbl/ext/Cameras/CChaseCamera.hpp" +#include "nbl/ext/Cameras/CDollyCamera.hpp" +#include "nbl/ext/Cameras/CDollyZoomCamera.hpp" +#include "nbl/ext/Cameras/CPathCamera.hpp" +#include "nbl/ext/Cameras/CCameraPreset.hpp" +#include "nbl/ext/Cameras/CCameraPresetFlow.hpp" +#include "nbl/ext/Cameras/CCameraKeyframeTrack.hpp" +#include "nbl/ext/Cameras/CCameraPlaybackTimeline.hpp" +#include "nbl/ext/Cameras/CCameraSequenceScript.hpp" +#include "nbl/ext/Cameras/CCameraScriptedRuntime.hpp" +#include "nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp" +#include "nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp" +#include "nbl/ext/Cameras/CCameraGoalAnalysis.hpp" +#include "nbl/ext/Cameras/CCameraGoalSolver.hpp" +#include "nbl/ext/Cameras/CCameraManipulationUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPresentationUtilities.hpp" +#include "nbl/ext/Cameras/CCameraProjectionUtilities.hpp" +#include "nbl/ext/Cameras/CCameraKindUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFollowUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp" +#include "camera/CCameraConstraintUtilities.hpp" +#include "camera/CCameraScriptedActionUtilities.hpp" +#include "camera/CCameraScriptedRuntimePersistence.hpp" +#include "camera/CCameraSequenceScriptedBuilder.hpp" +#include "camera/CCameraControlPanelUiUtilities.hpp" +#include "camera/CCameraScriptVisualDebugOverlayUtilities.hpp" +#include "camera/CCameraViewportOverlayUtilities.hpp" +#include "nbl/ext/Cameras/CCameraTextUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" +#include "nbl/ext/Cameras/CCubeProjection.hpp" +#include "nbl/ext/Cameras/CLinearProjection.hpp" +#include "nbl/ext/Cameras/CPlanarProjection.hpp" // the example's headers -#include "transform.hpp" - -using namespace nbl; -using namespace nbl::core; -using namespace nbl::hlsl; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::ui; -using namespace nbl::video; -using namespace nbl::examples; - -#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ \ No newline at end of file +#include "nbl/ui/ICursorControl.h" +#include "nbl/ext/ImGui/ImGui.h" +#include "imgui/imgui_internal.h" +#include "imguizmo/ImGuizmo.h" + +namespace nbl::this_example +{ + +template +inline hlsl::matrix getCastedMatrix(const hlsl::matrix& input) +{ + hlsl::matrix output; + for (uint32_t i = 0u; i < N; ++i) + output[i] = hlsl::CCameraMathUtilities::castVector(input[i]); + return output; +} + +} + +namespace core = nbl::core; +namespace asset = nbl::asset; +namespace ext = nbl::ext; +namespace ui = nbl::ui; +namespace video = nbl::video; +namespace examples = nbl::examples; +namespace hlsl = nbl::hlsl; +using nbl::core::bitflag; +using nbl::core::make_smart_refctd_ptr; +using nbl::core::smart_refctd_ptr; +using nbl::core::smart_refctd_ptr_static_cast; +using nbl::core::vector; +using nbl::system::path; +using nbl::system::ILogger; +using nbl::system::ISystem; +using nbl::system::IApplicationFramework; +using nbl::system::logger_opt_smart_ptr; +using nbl::asset::E_FORMAT; +using nbl::asset::EF_D32_SFLOAT; +using nbl::asset::EF_R16G16B16A16_SFLOAT; +using nbl::asset::EF_R8G8B8A8_SRGB; +using nbl::asset::EPBP_GRAPHICS; +using nbl::asset::ACCESS_FLAGS; +using nbl::asset::IAsset; +using nbl::asset::IAssetManager; +using nbl::asset::IAssetLoader; +using nbl::asset::IDescriptor; +using nbl::asset::IImage; +using nbl::asset::ISampler; +using nbl::asset::IShader; +using nbl::asset::PIPELINE_STAGE_FLAGS; +using nbl::asset::SBufferRange; +using nbl::asset::isDepthOrStencilFormat; +using nbl::ui::ICursorControl; +using nbl::ui::IKeyboardEventChannel; +using nbl::ui::IMouseEventChannel; +using nbl::ui::EKC_NONE; +using nbl::ui::EMC_NONE; +using nbl::ui::SKeyboardEvent; +using nbl::ui::SMouseEvent; +using nbl::ui::IWindow; +using nbl::ui::IWindowWin32; +using nbl::ui::stringToKeyCode; +using nbl::ui::stringToMouseCode; +using nbl::video::CSurfaceVulkanWin32; +using nbl::video::CSmoothResizeSurface; +using nbl::video::IDescriptorPool; +using nbl::video::IDeviceMemoryAllocation; +using nbl::video::ILogicalDevice; +using nbl::video::IQueue; +using nbl::video::ISemaphore; +using nbl::video::ISwapchain; +using nbl::video::ISmoothResizeSurface; +using nbl::video::IGPUBuffer; +using nbl::video::IGPUCommandBuffer; +using nbl::video::IGPUCommandPool; +using nbl::video::IGPUDescriptorSet; +using nbl::video::IGPUDescriptorSetLayout; +using nbl::video::IGPUFramebuffer; +using nbl::video::IGPUGraphicsPipeline; +using nbl::video::IGPUImage; +using nbl::video::IGPUImageView; +using nbl::video::IGPUPipelineBase; +using nbl::video::IGPURenderpass; +using nbl::video::IGPUSampler; +using nbl::video::SIntendedSubmitInfo; +using nbl::examples::CGeometryCreatorScene; +using nbl::examples::InputSystem; +using nbl::examples::CSimpleDebugRenderer; +using nbl::core::ICamera; +using nbl::core::CFPSCamera; +using nbl::core::CFreeCamera; +using nbl::core::CSphericalTargetCamera; +using nbl::core::COrbitCamera; +using nbl::core::CArcballCamera; +using nbl::core::CTurntableCamera; +using nbl::core::CTopDownCamera; +using nbl::core::CIsometricCamera; +using nbl::core::CChaseCamera; +using nbl::core::CDollyCamera; +using nbl::core::CDollyZoomCamera; +using nbl::core::CPathCamera; +using nbl::core::CCameraGoal; +using nbl::core::CTrackedTarget; +using nbl::core::CCameraPreset; +using nbl::core::CCameraKeyframe; +using nbl::core::CCameraKeyframeTrack; +using nbl::core::CCameraPlaybackCursor; +using nbl::core::CCameraSequenceScript; +using nbl::core::CCameraSequenceSegment; +using nbl::core::CCameraSequenceKeyframe; +using nbl::core::CCameraSequenceTrackedTargetPose; +using nbl::core::CCameraSequenceTrackedTargetTrack; +using nbl::core::CCameraSequencePresentation; +using nbl::core::CCameraSequenceContinuitySettings; +using nbl::system::CCameraScriptedInputEvent; +using nbl::system::CCameraScriptedInputCheck; +using nbl::system::CCameraScriptedFrameEvents; +using nbl::system::CCameraScriptedTimeline; +using nbl::system::CCameraScriptedCheckContext; +using nbl::system::CCameraScriptedCheckFrameResult; +using nbl::system::CCameraScriptedCheckLogEntry; +using nbl::system::CCameraScriptedCheckRuntimeState; +using nbl::core::SCameraPlaybackAdvanceResult; +using nbl::core::SCameraPresetApplySummary; +using nbl::core::SCameraGoalApplyAnalysis; +using nbl::core::SCameraCaptureAnalysis; +using nbl::core::SCameraFollowConfig; +using nbl::system::SCameraFollowVisualMetrics; +using nbl::ui::SCameraGoalApplyPresentation; +using nbl::ui::SCameraGoalApplyPresentationBadges; +using nbl::ui::SCameraCapturePresentation; +using nbl::this_example::ECameraScriptedActionCode; +using nbl::this_example::CCameraConstraintUtilities; +using nbl::this_example::CCameraScriptedActionUtilities; +using nbl::this_example::CCameraScriptedActionEvent; +using nbl::this_example::CCameraScriptedInputParseResult; +using nbl::this_example::CCameraScriptedRuntimePersistenceUtilities; +using nbl::this_example::CCameraSequenceScriptedSegmentBuildInfo; +using nbl::this_example::CCameraSequenceScriptedBuilderUtilities; +using nbl::this_example::SCameraConstraintSettings; +using nbl::core::CCameraGoalSolver; +using nbl::core::ECameraFollowMode; +using nbl::ui::EPresetApplyPresentationFilter; +using nbl::core::IPlanarProjection; +using nbl::core::CPlanarProjection; +using nbl::core::CVirtualGimbalEvent; +using nbl::ui::CGimbalInputBinder; +using nbl::ui::IGimbalBindingLayout; +using nbl::hlsl::float32_t; +using nbl::hlsl::float32_t2; +using nbl::hlsl::float32_t3; +using nbl::hlsl::float32_t4; +using nbl::hlsl::float32_t3x3; +using nbl::hlsl::float32_t3x4; +using nbl::hlsl::float32_t4x4; +using nbl::hlsl::float64_t; +using nbl::hlsl::float64_t3; +using nbl::hlsl::float64_t4; +using nbl::hlsl::float64_t4x4; +using nbl::hlsl::uint16_t2; +using nbl::hlsl::CCameraMathUtilities; +using nbl::ui::CCameraInputBindingUtilities; +using nbl::ui::CCameraControlPanelUiUtilities; +using nbl::ui::CCameraScriptVisualDebugOverlayUtilities; +using nbl::ui::CCameraTextUtilities; +using nbl::ui::CCameraViewportOverlayUtilities; +using nbl::this_example::getCastedMatrix; +using nbl::hlsl::mul; +using nbl::hlsl::camera_quaternion_t; +using nbl::core::CCameraFollowUtilities; +using nbl::core::CCameraProjectionUtilities; + +#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp new file mode 100644 index 000000000..ca79bce98 --- /dev/null +++ b/61_UI/include/keysmapping.hpp @@ -0,0 +1,9 @@ +#ifndef __NBL_KEYSMAPPING_H_INCLUDED__ +#define __NBL_KEYSMAPPING_H_INCLUDED__ + +#include "common.hpp" + +bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbalBindingLayout::BindingDomain activeBindingDomain, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode); +bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool spawnWindow = false); + +#endif // __NBL_KEYSMAPPING_H_INCLUDED__ diff --git a/61_UI/include/transform.hpp b/61_UI/include/transform.hpp deleted file mode 100644 index fb1672c2f..000000000 --- a/61_UI/include/transform.hpp +++ /dev/null @@ -1,162 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ - - -#include "nbl/ui/ICursorControl.h" - -#include "nbl/ext/ImGui/ImGui.h" - -#include "imgui/imgui_internal.h" -#include "imguizmo/ImGuizmo.h" - - -struct TransformRequestParams -{ - float camDistance = 8.f; - uint8_t sceneTexDescIx = ~0; - bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; -}; - -nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProjection, float* matrix, const TransformRequestParams& params) -{ - static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::TRANSLATE); - static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::LOCAL); - static bool useSnap = false; - static float snap[3] = { 1.f, 1.f, 1.f }; - static float bounds[] = { -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f }; - static float boundsSnap[] = { 0.1f, 0.1f, 0.1f }; - static bool boundSizing = false; - static bool boundSizingSnap = false; - - if (params.editTransformDecomposition) - { - if (ImGui::IsKeyPressed(ImGuiKey_T)) - mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - if (ImGui::IsKeyPressed(ImGuiKey_R)) - mCurrentGizmoOperation = ImGuizmo::ROTATE; - if (ImGui::IsKeyPressed(ImGuiKey_S)) - mCurrentGizmoOperation = ImGuizmo::SCALE; - if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) - mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - ImGui::SameLine(); - if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) - mCurrentGizmoOperation = ImGuizmo::ROTATE; - ImGui::SameLine(); - if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) - mCurrentGizmoOperation = ImGuizmo::SCALE; - if (ImGui::RadioButton("Universal", mCurrentGizmoOperation == ImGuizmo::UNIVERSAL)) - mCurrentGizmoOperation = ImGuizmo::UNIVERSAL; - float matrixTranslation[3], matrixRotation[3], matrixScale[3]; - ImGuizmo::DecomposeMatrixToComponents(matrix, matrixTranslation, matrixRotation, matrixScale); - ImGui::InputFloat3("Tr", matrixTranslation); - ImGui::InputFloat3("Rt", matrixRotation); - ImGui::InputFloat3("Sc", matrixScale); - ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix); - - if (mCurrentGizmoOperation != ImGuizmo::SCALE) - { - if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) - mCurrentGizmoMode = ImGuizmo::LOCAL; - ImGui::SameLine(); - if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) - mCurrentGizmoMode = ImGuizmo::WORLD; - } - if (ImGui::IsKeyPressed(ImGuiKey_S) && ImGui::IsKeyPressed(ImGuiKey_LeftShift)) - useSnap = !useSnap; - ImGui::Checkbox("##UseSnap", &useSnap); - ImGui::SameLine(); - - switch (mCurrentGizmoOperation) - { - case ImGuizmo::TRANSLATE: - ImGui::InputFloat3("Snap", &snap[0]); - break; - case ImGuizmo::ROTATE: - ImGui::InputFloat("Angle Snap", &snap[0]); - break; - case ImGuizmo::SCALE: - ImGui::InputFloat("Scale Snap", &snap[0]); - break; - } - ImGui::Checkbox("Bound Sizing", &boundSizing); - if (boundSizing) - { - ImGui::PushID(3); - ImGui::Checkbox("##BoundSizing", &boundSizingSnap); - ImGui::SameLine(); - ImGui::InputFloat3("Snap", boundsSnap); - ImGui::PopID(); - } - } - - ImGuiIO& io = ImGui::GetIO(); - float viewManipulateRight = io.DisplaySize.x; - float viewManipulateTop = 0; - static ImGuiWindowFlags gizmoWindowFlags = 0; - - /* - for the "useWindow" case we just render to a gui area, - otherwise to fake full screen transparent window - - note that for both cases we make sure gizmo being - rendered is aligned to our texture scene using - imgui "cursor" screen positions - */ -// TODO: this shouldn't be handled here I think - SImResourceInfo info; - info.textureID = params.sceneTexDescIx; - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - - nbl::hlsl::uint16_t2 retval; - if (params.useWindow) - { - ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); - ImGui::SetNextWindowPos(ImVec2(400, 20), ImGuiCond_Appearing); - ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - ImGui::Begin("Gizmo", 0, gizmoWindowFlags); - ImGuizmo::SetDrawlist(); - - ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); - ImVec2 windowPos = ImGui::GetWindowPos(); - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - retval = {contentRegionSize.x,contentRegionSize.y}; - - viewManipulateRight = cursorPos.x + contentRegionSize.x; - viewManipulateTop = cursorPos.y; - - ImGuiWindow* window = ImGui::GetCurrentWindow(); - gizmoWindowFlags = (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max) ? ImGuiWindowFlags_NoMove : 0); - } - else - { - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(io.DisplaySize); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // fully transparent fake window - ImGui::Begin("FullScreenWindow", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs); - - ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - retval = {contentRegionSize.x,contentRegionSize.y}; - - viewManipulateRight = cursorPos.x + contentRegionSize.x; - viewManipulateTop = cursorPos.y; - } - - ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, useSnap ? &snap[0] : NULL, boundSizing ? bounds : NULL, boundSizingSnap ? boundsSnap : NULL); - - if(params.enableViewManipulate) - ImGuizmo::ViewManipulate(cameraView, params.camDistance, ImVec2(viewManipulateRight - 128, viewManipulateTop), ImVec2(128, 128), 0x10101010); - - ImGui::End(); - ImGui::PopStyleColor(); - - return retval; -} - -#endif // __NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/main.cpp b/61_UI/main.cpp index e41b7d827..9cb5271da 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1,972 +1,3 @@ -// Copyright (C) 2018-2026 DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h +#include "app/App.hpp" -#include "common.hpp" -#include - -/* -Renders scene texture to an offscreen framebuffer whose color attachment is then sampled into a imgui window. - -Written with Nabla's UI extension and got integrated with ImGuizmo to handle scene's object translations. -*/ -class UISampleApp final : public MonoWindowApplication, public BuiltinResourcesApplication -{ - using device_base_t = MonoWindowApplication; - using asset_base_t = BuiltinResourcesApplication; - - public: - inline UISampleApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), - device_base_t({1280,720}, EF_UNKNOWN, _localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - auto pool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - for (auto i = 0u; icreateCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{m_cmdBufs.data()+i,1})) - return logFail("Couldn't create Command Buffer!"); - } - - const uint32_t addtionalBufferOwnershipFamilies[] = {getGraphicsQueue()->getFamilyIndex()}; - m_scene = CGeometryCreatorScene::create( - { - .transferQueue = getTransferUpQueue(), - .utilities = m_utils.get(), - .logger = m_logger.get(), - .addtionalBufferOwnershipFamilies = addtionalBufferOwnershipFamilies - }, - CSimpleDebugRenderer::DefaultPolygonGeometryPatch - ); - - // for the scene drawing pass - { - IGPURenderpass::SCreationParams params = {}; - const IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { - {{ - { - .format = sceneRenderDepthFormat, - .samples = IGPUImage::ESCF_1_BIT, - .mayAlias = false - }, - /*.loadOp = */{IGPURenderpass::LOAD_OP::CLEAR}, - /*.storeOp = */{IGPURenderpass::STORE_OP::STORE}, - /*.initialLayout = */{IGPUImage::LAYOUT::UNDEFINED}, - /*.finalLayout = */{IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} - }}, - IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd - }; - params.depthStencilAttachments = depthAttachments; - const IGPURenderpass::SCreationParams::SColorAttachmentDescription colorAttachments[] = { - {{ - { - .format = finalSceneRenderFormat, - .samples = IGPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT, - .mayAlias = false - }, - /*.loadOp = */IGPURenderpass::LOAD_OP::CLEAR, - /*.storeOp = */IGPURenderpass::STORE_OP::STORE, - /*.initialLayout = */IGPUImage::LAYOUT::UNDEFINED, - /*.finalLayout = */ IGPUImage::LAYOUT::READ_ONLY_OPTIMAL // ImGUI shall read - }}, - IGPURenderpass::SCreationParams::ColorAttachmentsEnd - }; - params.colorAttachments = colorAttachments; - IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - {}, - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].depthStencilAttachment = {{.render={.attachmentIndex=0,.layout=IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL}}}; - subpasses[0].colorAttachments[0] = {.render={.attachmentIndex=0,.layout=IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL}}; - params.subpasses = subpasses; - - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - // wipe-transition of Color to ATTACHMENT_OPTIMAL and depth - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - // last place where the depth can get modified in previous frame, `COLOR_ATTACHMENT_OUTPUT_BIT` is implicitly later - // while color is sampled by ImGUI - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT|PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, - // don't want any writes to be available, as we are clearing both attachments - .srcAccessMask = ACCESS_FLAGS::NONE, - // destination needs to wait as early as possible - // TODO: `COLOR_ATTACHMENT_OUTPUT_BIT` shouldn't be needed, because its a logically later stage, see TODO in `ECommonEnums.h` - .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT|PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - // because depth and color get cleared first no read mask - .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT|ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - // leave view offsets and flags default - }, - { - .srcSubpass = 0, - .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .memoryBarrier = { - // last place where the color can get modified, depth is implicitly earlier - .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - // only write ops, reads can't be made available, also won't be using depth so don't care about it being visible to anyone else - .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, - // the ImGUI will sample the color, then next frame we overwrite both attachments - .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT|PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT, - // but we only care about the availability-visibility chain between renderpass and imgui - .dstAccessMask = ACCESS_FLAGS::SAMPLED_READ_BIT - } - // leave view offsets and flags default - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - params.dependencies = {}; - m_renderpass = m_device->createRenderpass(std::move(params)); - if (!m_renderpass) - return logFail("Failed to create Scene Renderpass!"); - } - const auto& geometries = m_scene->getInitParams().geometries; - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(),m_renderpass.get(),0,{&geometries.front().get(),geometries.size()}); - // special case - { - const auto& pipelines = m_renderer->getInitParams().pipelines; - auto ix = 0u; - for (const auto& name : m_scene->getInitParams().geometryNames) - { - if (name=="Cone") - m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; - ix++; - } - } - // we'll only display one thing at a time - m_renderer->m_instances.resize(1); - - // Create Frustum Drawer - { - SPushConstantRange simplePcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, - .offset = offsetof(ext::frustum::PushConstants, spc), - .size = sizeof(ext::frustum::SSinglePC) - }; - ext::frustum::CDrawFrustum::SCreationParameters params = {}; - params.transfer = getTransferUpQueue(); - params.assetManager = m_assetMgr; - params.drawMode = ext::frustum::CDrawFrustum::DM_BOTH; - params.singlePipelineLayout = ext::frustum::CDrawFrustum::createPipelineLayoutFromPCRange(m_device.get(), simplePcRange); - params.batchPipelineLayout = ext::frustum::CDrawFrustum::createDefaultPipelineLayout(m_device.get()); - params.renderpass = smart_refctd_ptr(m_renderpass); - params.utilities = m_utils; - m_drawFrustum = ext::frustum::CDrawFrustum::create(std::move(params)); - if (!m_drawFrustum) - return logFail("Failed to create Frustum Drawer!"); - } - - // Create ImGUI - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - ext::imgui::UI::SCreationParameters params = {}; - params.resources.texturesInfo = {.setIx=0u,.bindingIx=TexturesImGUIBindingIndex}; - params.resources.samplersInfo = {.setIx=0u,.bindingIx=1u}; - params.utilities = m_utils; - params.transfer = getTransferUpQueue(); - params.pipelineLayout = ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(),params.resources.texturesInfo,params.resources.samplersInfo,MaxImGUITextures); - params.assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - params.renderpass = smart_refctd_ptr(scRes->getRenderpass()); - params.subpassIx = 0u; - params.pipelineCache = nullptr; - interface.imGUI = ext::imgui::UI::create(std::move(params)); - if (!interface.imGUI) - return logFail("Failed to create `nbl::ext::imgui::UI` class"); - } - - // create rest of User Interface - { - auto* imgui = interface.imGUI.get(); - // create the suballocated descriptor set - { - // note that we use default layout provided by our extension, but you are free to create your own by filling ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* layout = imgui->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - auto pool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT,{&layout,1}); - auto ds = pool->createDescriptorSet(smart_refctd_ptr(layout)); - interface.subAllocDS = make_smart_refctd_ptr(std::move(ds)); - if (!interface.subAllocDS) - return logFail("Failed to create the descriptor set"); - // make sure Texture Atlas slot is taken for eternity - { - auto dummy = SubAllocatedDescriptorSet::invalid_value; - interface.subAllocDS->multi_allocate(0,1,&dummy); - assert(dummy==ext::imgui::UI::FontAtlasTexId); - } - // write constant descriptors, note we don't create info & write pair for the samplers because UI extension's are immutable and baked into DS layout - IGPUDescriptorSet::SDescriptorInfo info = {}; - info.desc = smart_refctd_ptr(interface.imGUI->getFontAtlasView()); - info.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - const IGPUDescriptorSet::SWriteDescriptorSet write = { - .dstSet = interface.subAllocDS->getDescriptorSet(), - .binding = TexturesImGUIBindingIndex, - .arrayElement = ext::imgui::UI::FontAtlasTexId, - .count = 1, - .info = &info - }; - if (!m_device->updateDescriptorSets({&write,1},{})) - return logFail("Failed to write the descriptor set"); - } - imgui->registerListener([this](){interface();}); - } - - interface.camera.mapKeysToArrows(); - - onAppInitializedFinish(); - return true; - } - - // - virtual inline bool onAppTerminated() - { - SubAllocatedDescriptorSet::value_type fontAtlasDescIx = ext::imgui::UI::FontAtlasTexId; - IGPUDescriptorSet::SDropDescriptorSet dummy[1]; - interface.subAllocDS->multi_deallocate(dummy,TexturesImGUIBindingIndex,1,&fontAtlasDescIx); - return device_base_t::onAppTerminated(); - } - - inline IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override - { - // CPU events - update(nextPresentationTimestamp); - - const auto& virtualWindowRes = interface.sceneResolution; - if (!m_framebuffer || m_framebuffer->getCreationParameters().width!=virtualWindowRes[0] || m_framebuffer->getCreationParameters().height!=virtualWindowRes[1]) - recreateFramebuffer(virtualWindowRes); - - // - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - auto* const cb = m_cmdBufs.data()[resourceIx].get(); - cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - // clear to black for both things - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - if (m_framebuffer) - { - cb->beginDebugMarker("UISampleApp Scene Frame"); - { - const IGPUCommandBuffer::SClearDepthStencilValue farValue = { .depth=0.f }; - const IGPUCommandBuffer::SRenderpassBeginInfo renderpassInfo = - { - .framebuffer = m_framebuffer.get(), - .colorClearValues = &clearValue, - .depthStencilClearValues = &farValue, - .renderArea = { - .offset = {0,0}, - .extent = {virtualWindowRes[0],virtualWindowRes[1]} - } - }; - beginRenderpass(cb,renderpassInfo); - } - // draw scene - { - // Select active camera for viewing - const auto& viewCamera = interface.useDebugCameraView ? interface.debugCamera : interface.camera; - float32_t3x4 viewMatrix = viewCamera.getViewMatrix(); - float32_t4x4 viewProjMatrix = viewCamera.getConcatenatedMatrix(); - const auto viewParams = CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix); - - // tear down scene every frame - auto& instance = m_renderer->m_instances[0]; - memcpy(&instance.world,&interface.model,sizeof(instance.world)); - instance.packedGeo = m_renderer->getGeometries().data() + interface.gcIndex; - m_renderer->render(cb,viewParams); - } - // Always draw debug camera's frustum — viewed from whichever camera is active. - if (interface.showFrustum) - { - const auto& viewCamera = interface.useDebugCameraView ? interface.debugCamera : interface.camera; - - ext::frustum::CDrawFrustum::DrawParameters drawParams; - drawParams.commandBuffer = cb; - drawParams.viewProjectionMatrix = viewCamera.getConcatenatedMatrix(); - drawParams.lineWidth = 1.0f; - - hlsl::float32_t4x4 frustumCameraViewProj = interface.debugCamera.getConcatenatedMatrix(); - hlsl::float32_t4x4 frustumTransform = hlsl::inverse(frustumCameraViewProj); - - hlsl::float32_t4 color = {1.0f, 1.0f, 1.0f, 1.0f}; - - if (!m_drawFrustum->renderSingle(drawParams, frustumTransform, color)) - m_logger->log("Failed to draw frustum!", ILogger::ELL_ERROR); - } - cb->endRenderPass(); - cb->endDebugMarker(); - } - { - cb->beginDebugMarker("UISampleApp IMGUI Frame"); - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - const IGPUCommandBuffer::SRenderpassBeginInfo renderpassInfo = - { - .framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex), - .colorClearValues = &clearValue, - .depthStencilClearValues = nullptr, - .renderArea = { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - } - }; - beginRenderpass(cb,renderpassInfo); - } - // draw ImGUI - { - auto* imgui = interface.imGUI.get(); - auto* pipeline = imgui->getPipeline(); - cb->bindGraphicsPipeline(pipeline); - // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx - const auto* ds = interface.subAllocDS->getDescriptorSet(); - cb->bindDescriptorSets(EPBP_GRAPHICS,pipeline->getLayout(),imgui->getCreationParameters().resources.texturesInfo.setIx,1u,&ds); - // a timepoint in the future to release streaming resources for geometry - const ISemaphore::SWaitInfo drawFinished = {.semaphore=m_semaphore.get(),.value=m_realFrameIx+1u}; - if (!imgui->render(cb,drawFinished)) - { - m_logger->log("TODO: need to present acquired image before bailing because its already acquired.",ILogger::ELL_ERROR); - return {}; - } - } - cb->endRenderPass(); - cb->endDebugMarker(); - } - cb->end(); - - //updateGUIDescriptorSet(); - - IQueue::SSubmitInfo::SSemaphoreInfo retval = - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS - }; - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cb } - }; - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = { - { - .semaphore = device_base_t::getCurrentAcquire().semaphore, - .value = device_base_t::getCurrentAcquire().acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = {&retval,1} - } - }; - - if (getGraphicsQueue()->submit(infos) != IQueue::RESULT::SUCCESS) - { - retval.semaphore = nullptr; // so that we don't wait on semaphore that will never signal - m_realFrameIx--; - } - - - m_window->setCaption("[Nabla Engine] UI App Test Demo"); - return retval; - } - - protected: - const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override - { - // Subsequent submits don't wait for each other, but they wait for acquire and get waited on by present - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - // don't want any writes to be available, we'll clear, only thing to worry about is the layout transition - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, // should sync against the semaphore wait anyway - .srcAccessMask = ACCESS_FLAGS::NONE, - // layout transition needs to finish before the color write - .dstStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - // leave view offsets and flags default - }, - // want layout transition to begin after all color output is done - { - .srcSubpass = 0, - .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .memoryBarrier = { - // last place where the color can get modified, depth is implicitly earlier - .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - // only write ops, reads can't be made available - .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - // spec says nothing is needed when presentation is the destination - } - // leave view offsets and flags default - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - return dependencies; - } - - private: - inline void update(const std::chrono::microseconds nextPresentationTimestamp) - { - auto& camera = interface.camera; - camera.setMoveSpeed(interface.moveSpeed); - camera.setRotateSpeed(interface.rotateSpeed); - - - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - struct - { - std::vector mouse{}; - std::vector keyboard{}; - } uiEvents; - - // TODO: should be a member really - static std::chrono::microseconds previousEventTimestamp{}; - - // I think begin/end should always be called on camera, just events shouldn't be fed, why? - // If you stop begin/end, whatever keys were up/down get their up/down values frozen leading to - // `perActionDt` becoming obnoxiously large the first time the even processing resumes due to - // `timeDiff` being computed since `lastVirtualUpTimeStamp` - camera.beginInputProcessing(nextPresentationTimestamp); - { - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void - { - if (interface.move) - camera.mouseProcess(events); // don't capture the events, only let camera handle them with its impl - - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - uiEvents.mouse.emplace_back(e); - - if (e.type==nbl::ui::SMouseEvent::EET_SCROLL && m_renderer) - { - interface.gcIndex += int16_t(core::sign(e.scrollEvent.verticalScroll)); - interface.gcIndex = core::clamp(interface.gcIndex,0ull,m_renderer->getGeometries().size()-1); - } - } - }, - m_logger.get() - ); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void - { - if (interface.move) - camera.keyboardProcess(events); // don't capture the events, only let camera handle them with its impl - - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - uiEvents.keyboard.emplace_back(e); - } - }, - m_logger.get() - ); - } - camera.endInputProcessing(nextPresentationTimestamp); - - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - - ext::imgui::UI::SUpdateParameters params = - { - .mousePosition = float32_t2(cursorPosition.x,cursorPosition.y) - float32_t2(m_window->getX(),m_window->getY()), - .displaySize = {m_window->getWidth(),m_window->getHeight()}, - .mouseEvents = uiEvents.mouse, - .keyboardEvents = uiEvents.keyboard - }; - - interface.objectName = m_scene->getInitParams().geometryNames[interface.gcIndex]; - interface.imGUI->update(params); - } - - void recreateFramebuffer(const uint16_t2 resolution) - { - auto createImageAndView = [&](E_FORMAT format)->smart_refctd_ptr - { - auto image = m_device->createImage({{ - .type = IGPUImage::ET_2D, - .samples = IGPUImage::ESCF_1_BIT, - .format = format, - .extent = {resolution.x,resolution.y,1}, - .mipLevels = 1, - .arrayLayers = 1, - .usage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT|IGPUImage::EUF_SAMPLED_BIT - }}); - if (!m_device->allocate(image->getMemoryReqs(),image.get()).isValid()) - return nullptr; - IGPUImageView::SCreationParams params = { - .image = std::move(image), - .viewType = IGPUImageView::ET_2D, - .format = format - }; - params.subresourceRange.aspectMask = isDepthOrStencilFormat(format) ? IGPUImage::EAF_DEPTH_BIT:IGPUImage::EAF_COLOR_BIT; - return m_device->createImageView(std::move(params)); - }; - - smart_refctd_ptr colorView; - // detect window minimization - if (resolution.x<0x4000 && resolution.y<0x4000) - { - colorView = createImageAndView(finalSceneRenderFormat); - auto depthView = createImageAndView(sceneRenderDepthFormat); - m_framebuffer = m_device->createFramebuffer({ { - .renderpass = m_renderpass, - .depthStencilAttachments = &depthView.get(), - .colorAttachments = &colorView.get(), - .width = resolution.x, - .height = resolution.y - }}); - } - else - m_framebuffer = nullptr; - - // release previous slot and its image - interface.subAllocDS->multi_deallocate(0,1,&interface.renderColorViewDescIndex,{.semaphore=m_semaphore.get(),.value=m_realFrameIx}); - // - if (colorView) - { - interface.subAllocDS->multi_allocate(0,1,&interface.renderColorViewDescIndex); - // update descriptor set - IGPUDescriptorSet::SDescriptorInfo info = {}; - info.desc = colorView; - info.info.image.imageLayout = IGPUImage::LAYOUT::READ_ONLY_OPTIMAL; - const IGPUDescriptorSet::SWriteDescriptorSet write = { - .dstSet = interface.subAllocDS->getDescriptorSet(), - .binding = TexturesImGUIBindingIndex, - .arrayElement = interface.renderColorViewDescIndex, - .count = 1, - .info = &info - }; - m_device->updateDescriptorSets({&write,1},{}); - } - interface.transformParams.sceneTexDescIx = interface.renderColorViewDescIndex; - } - - inline void beginRenderpass(IGPUCommandBuffer* cb, const IGPUCommandBuffer::SRenderpassBeginInfo& info) - { - cb->beginRenderPass(info,IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - cb->setScissor(0,1,&info.renderArea); - const SViewport viewport = { - .x = 0, - .y = 0, - .width = static_cast(info.renderArea.extent.width), - .height = static_cast(info.renderArea.extent.height) - }; - cb->setViewport(0u,1u,&viewport); - } - - // Maximum frames which can be simultaneously submitted, used to cycle through our per-frame resources like command buffers - constexpr static inline uint32_t MaxFramesInFlight = 3u; - constexpr static inline auto sceneRenderDepthFormat = EF_D32_SFLOAT; - constexpr static inline auto finalSceneRenderFormat = EF_R8G8B8A8_SRGB; - constexpr static inline auto TexturesImGUIBindingIndex = 0u; - // we create the Descriptor Set with a few slots extra to spare, so we don't have to `waitIdle` the device whenever ImGUI virtual window resizes - constexpr static inline auto MaxImGUITextures = 2u+MaxFramesInFlight; - - // - smart_refctd_ptr m_scene; - smart_refctd_ptr m_renderpass; - smart_refctd_ptr m_renderer; - smart_refctd_ptr m_framebuffer; - smart_refctd_ptr m_drawFrustum; - // - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array,MaxFramesInFlight> m_cmdBufs; - // - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - // UI stuff - struct CInterface - { - void operator()() - { - ImGuiIO& io = ImGui::GetIO(); - - // TODO: why is this a lambda and not just an assignment in a scope ? - camera.setProjectionMatrix([&]() - { - hlsl::float32_t4x4 projection; - - if (isPerspective) - if(isLH) - projection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); - else - projection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(core::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); - else - { - float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; - - if(isLH) - projection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(viewWidth, viewHeight, zNear, zFar); - else - projection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(viewWidth, viewHeight, zNear, zFar); - } - - return projection; - }()); - - // Debug camera projection has its own LH/RH and perspective/ortho toggles. - debugCamera.setProjectionMatrix([&]() - { - hlsl::float32_t4x4 projection; - if (debugIsPerspective) - if (debugIsLH) - projection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(debugFov), io.DisplaySize.x / io.DisplaySize.y, debugCamZNear, debugCamZFar); - else - projection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(core::radians(debugFov), io.DisplaySize.x / io.DisplaySize.y, debugCamZNear, debugCamZFar); - else - { - float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; - if (debugIsLH) - projection = hlsl::math::thin_lens::lhProjectionOrthoMatrix(viewWidth, viewHeight, debugCamZNear, debugCamZFar); - else - projection = hlsl::math::thin_lens::rhProjectionOrthoMatrix(viewWidth, viewHeight, debugCamZNear, debugCamZFar); - } - return projection; - }()); - - ImGuizmo::SetOrthographic(false); - ImGuizmo::BeginFrame(); - - ImGui::SetNextWindowPos(ImVec2(1024, 100), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(256, 256), ImGuiCond_Appearing); - - // create a window and insert the inspector - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); - ImGui::Begin("Editor"); - - if (ImGui::RadioButton("Full view", !transformParams.useWindow)) - transformParams.useWindow = false; - - ImGui::SameLine(); - - if (ImGui::RadioButton("Window", transformParams.useWindow)) - transformParams.useWindow = true; - - ImGui::Text("Camera"); - bool viewDirty = false; - - if (ImGui::RadioButton("LH", isLH)) - isLH = true; - - ImGui::SameLine(); - - if (ImGui::RadioButton("RH", !isLH)) - isLH = false; - - if (ImGui::RadioButton("Perspective", isPerspective)) - isPerspective = true; - - ImGui::SameLine(); - - if (ImGui::RadioButton("Orthographic", !isPerspective)) - isPerspective = false; - - ImGui::Checkbox("Enable \"view manipulate\"", &transformParams.enableViewManipulate); - ImGui::Checkbox("Enable camera movement", &move); - ImGui::SliderFloat("Move speed", &moveSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Rotate speed", &rotateSpeed, 0.1f, 10.f); - - // ImGui::Checkbox("Flip Gizmo's Y axis", &flipGizmoY); // let's not expose it to be changed in UI but keep the logic in case - - if (isPerspective) - ImGui::SliderFloat("Fov", &fov, 20.f, 150.f); - else - ImGui::SliderFloat("Ortho width", &viewWidth, 1, 20); - - ImGui::SliderFloat("zNear", &zNear, 0.1f, 100.f); - ImGui::SliderFloat("zFar", &zFar, 110.f, 10000.f); - - viewDirty |= ImGui::SliderFloat("Distance", &transformParams.camDistance, 1.f, 69.f); - - // Frustum Visualization Controls - ImGui::Separator(); - ImGui::Text("Frustum Debug Visualization"); - ImGui::Checkbox("Show Debug Camera Frustum", &showFrustum); - ImGui::Checkbox("Use Debug Camera View", &useDebugCameraView); - if (showFrustum) - { - if (ImGui::RadioButton("Debug LH", debugIsLH)) - debugIsLH = true; - ImGui::SameLine(); - if (ImGui::RadioButton("Debug RH", !debugIsLH)) - debugIsLH = false; - if (ImGui::RadioButton("Debug Perspective", debugIsPerspective)) - debugIsPerspective = true; - ImGui::SameLine(); - if (ImGui::RadioButton("Debug Orthographic", !debugIsPerspective)) - debugIsPerspective = false; - if (debugIsPerspective) - ImGui::SliderFloat("Debug Fov", &debugFov, 20.f, 150.f); - ImGui::SliderFloat("Debug Cam zNear", &debugCamZNear, 0.1f, 5.f); - ImGui::SliderFloat("Debug Cam zFar", &debugCamZFar, 5.f, 50.f); - } - - if (viewDirty || firstFrame) - { - core::vectorSIMDf cameraPosition(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); - core::vectorSIMDf cameraTarget(0.f, 0.f, 0.f); - const static core::vectorSIMDf up(0.f, 1.f, 0.f); - - camera.setPosition(cameraPosition); - camera.setTarget(cameraTarget); - camera.setBackupUpVector(up); - - camera.recomputeViewMatrix(); - } - firstFrame = false; - - ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); - if (ImGuizmo::IsUsing()) - { - ImGui::Text("Using gizmo"); - } - else - { - ImGui::Text(ImGuizmo::IsOver() ? "Over gizmo" : ""); - ImGui::SameLine(); - ImGui::Text(ImGuizmo::IsOver(ImGuizmo::TRANSLATE) ? "Over translate gizmo" : ""); - ImGui::SameLine(); - ImGui::Text(ImGuizmo::IsOver(ImGuizmo::ROTATE) ? "Over rotate gizmo" : ""); - ImGui::SameLine(); - ImGui::Text(ImGuizmo::IsOver(ImGuizmo::SCALE) ? "Over scale gizmo" : ""); - } - ImGui::Separator(); - - /* - * ImGuizmo expects view & perspective matrix to be column major both with 4x4 layout - * and Nabla uses row major matricies - 3x4 matrix for view & 4x4 for projection - - - VIEW: - - ImGuizmo - - | X[0] Y[0] Z[0] 0.0f | - | X[1] Y[1] Z[1] 0.0f | - | X[2] Y[2] Z[2] 0.0f | - | -Dot(X, eye) -Dot(Y, eye) -Dot(Z, eye) 1.0f | - - Nabla - - | X[0] X[1] X[2] -Dot(X, eye) | - | Y[0] Y[1] Y[2] -Dot(Y, eye) | - | Z[0] Z[1] Z[2] -Dot(Z, eye) | - - = transpose(nbl::core::matrix4SIMD()) - - - PERSPECTIVE [PROJECTION CASE]: - - ImGuizmo - - | (temp / temp2) (0.0) (0.0) (0.0) | - | (0.0) (temp / temp3) (0.0) (0.0) | - | ((right + left) / temp2) ((top + bottom) / temp3) ((-zfar - znear) / temp4) (-1.0f) | - | (0.0) (0.0) ((-temp * zfar) / temp4) (0.0) | - - Nabla - - | w (0.0) (0.0) (0.0) | - | (0.0) -h (0.0) (0.0) | - | (0.0) (0.0) (-zFar/(zFar-zNear)) (-zNear*zFar/(zFar-zNear)) | - | (0.0) (0.0) (-1.0) (0.0) | - - = transpose() - - * - * the ViewManipulate final call (inside EditTransform) returns world space column major matrix for an object, - * note it also modifies input view matrix but projection matrix is immutable - */ - - static struct - { - hlsl::float32_t4x4 view, projection, model; - } imguizmoM16InOut; - - ImGuizmo::SetID(0u); - - imguizmoM16InOut.view = hlsl::transpose(hlsl::math::linalg::promote_affine<4,4,3,4>(camera.getViewMatrix())); - imguizmoM16InOut.projection = hlsl::transpose(camera.getProjectionMatrix()); - imguizmoM16InOut.model = hlsl::transpose(hlsl::math::linalg::promote_affine<4,4,3,4>(model)); - { - if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates - imguizmoM16InOut.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - - transformParams.editTransformDecomposition = true; - sceneResolution = EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], transformParams); - } - - model = hlsl::math::linalg::truncate<3,4,4,4>(hlsl::transpose(imguizmoM16InOut.model)); - // to Nabla + update camera & model matrices -// TODO: make it more nicely, extract: -// - Position by computing inverse of the view matrix and grabbing its translation -// - Target from 3rd row without W component of view matrix multiplied by some arbitrary distance value (can be the length of position from origin) and adding the position -// But then set the view matrix this way anyway, because up-vector may not be compatible - const auto& view = camera.getViewMatrix(); - const_cast(view) = hlsl::math::linalg::truncate<3,4,4,4>(hlsl::transpose(imguizmoM16InOut.view)); // a hack, correct way would be to use inverse matrix and get position + target because now it will bring you back to last position & target when switching from gizmo move to manual move (but from manual to gizmo is ok) - // update concatanated matrix - const auto& projection = camera.getProjectionMatrix(); - camera.setProjectionMatrix(projection); - - // object meta display - { - ImGui::Begin("Object"); - ImGui::Text("type: \"%s\"", objectName.data()); - ImGui::End(); - } - - // view matrices editor - { - ImGui::Begin("Matrices"); - - auto addMatrixTable = [&](const char* topText, const char* tableName, const int rows, const int columns, const float* pointer, const bool withSeparator = true) - { - ImGui::Text(topText); - if (ImGui::BeginTable(tableName, columns)) - { - for (int y = 0; y < rows; ++y) - { - ImGui::TableNextRow(); - for (int x = 0; x < columns; ++x) - { - ImGui::TableSetColumnIndex(x); - ImGui::Text("%.3f", *(pointer + (y * columns) + x)); - } - } - ImGui::EndTable(); - } - - if (withSeparator) - ImGui::Separator(); - }; - - addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &model[0][0]); - addMatrixTable("Camera View Matrix", "ViewMatrixTable", 3, 4, &view[0][0]); - addMatrixTable("Camera View Projection Matrix", "ViewProjectionMatrixTable", 4, 4, &projection[0][0], false); - - ImGui::End(); - } - - // Nabla Imgui backend MDI buffer info - // To be 100% accurate and not overly conservative we'd have to explicitly `cull_frees` and defragment each time, - // so unless you do that, don't use this basic info to optimize the size of your IMGUI buffer. - { - auto* streaminingBuffer = imGUI->getStreamingBuffer(); - - const size_t total = streaminingBuffer->get_total_size(); // total memory range size for which allocation can be requested - const size_t freeSize = streaminingBuffer->getAddressAllocator().get_free_size(); // max total free bloock memory size we can still allocate from total memory available - const size_t consumedMemory = total - freeSize; // memory currently consumed by streaming buffer - - float freePercentage = 100.0f * (float)(freeSize) / (float)total; - float allocatedPercentage = (float)(consumedMemory) / (float)total; - - ImVec2 barSize = ImVec2(400, 30); - float windowPadding = 10.0f; - float verticalPadding = ImGui::GetStyle().FramePadding.y; - - ImGui::SetNextWindowSize(ImVec2(barSize.x + 2 * windowPadding, 110 + verticalPadding), ImGuiCond_Always); - ImGui::Begin("Nabla Imgui MDI Buffer Info", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar); - - ImGui::Text("Total Allocated Size: %zu bytes", total); - ImGui::Text("In use: %zu bytes", consumedMemory); - ImGui::Text("Buffer Usage:"); - - ImGui::SetCursorPosX(windowPadding); - - if (freePercentage > 70.0f) - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.0f, 1.0f, 0.0f, 0.4f)); // Green - else if (freePercentage > 30.0f) - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1.0f, 1.0f, 0.0f, 0.4f)); // Yellow - else - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1.0f, 0.0f, 0.0f, 0.4f)); // Red - - ImGui::ProgressBar(allocatedPercentage, barSize, ""); - - ImGui::PopStyleColor(); - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - ImVec2 progressBarPos = ImGui::GetItemRectMin(); - ImVec2 progressBarSize = ImGui::GetItemRectSize(); - - const char* text = "%.2f%% free"; - char textBuffer[64]; - snprintf(textBuffer, sizeof(textBuffer), text, freePercentage); - - ImVec2 textSize = ImGui::CalcTextSize(textBuffer); - ImVec2 textPos = ImVec2 - ( - progressBarPos.x + (progressBarSize.x - textSize.x) * 0.5f, - progressBarPos.y + (progressBarSize.y - textSize.y) * 0.5f - ); - - ImVec4 bgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); - drawList->AddRectFilled - ( - ImVec2(textPos.x - 5, textPos.y - 2), - ImVec2(textPos.x + textSize.x + 5, textPos.y + textSize.y + 2), - ImGui::GetColorU32(bgColor) - ); - - ImGui::SetCursorScreenPos(textPos); - ImGui::Text("%s", textBuffer); - - ImGui::Dummy(ImVec2(0.0f, verticalPadding)); - - ImGui::End(); - } - - ImGui::End(); - } - - smart_refctd_ptr imGUI; - // descriptor set - smart_refctd_ptr subAllocDS; - SubAllocatedDescriptorSet::value_type renderColorViewDescIndex = SubAllocatedDescriptorSet::invalid_value; - // - // Main camera: positioned to see both the object and the frustum - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); - // Debug camera: positioned closer to the object (frustum will visualize what this camera sees) - Camera debugCamera = Camera(core::vectorSIMDf(3, 2, 3), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); - // mutables - hlsl::float32_t3x4 model = hlsl::math::linalg::diagonal(1.0f); - std::string_view objectName; - TransformRequestParams transformParams; - uint16_t2 sceneResolution = {1280,720}; - float fov = 60.f, zNear = 0.1f, zFar = 10000.f, moveSpeed = 1.f, rotateSpeed = 1.f; - float viewWidth = 10.f; - float camYAngle = 165.f / 180.f * 3.14159f; - float camXAngle = 32.f / 180.f * 3.14159f; - uint16_t gcIndex = {}; // note: this is dirty however since I assume only single object in scene I can leave it now, when this example is upgraded to support multiple objects this needs to be changed - bool isPerspective = true, isLH = true, flipGizmoY = true, move = false; - bool showFrustum = true; // Toggle frustum visualization - float debugFov = 60.f, debugCamZNear = 0.5f, debugCamZFar = 20.0f; - bool useDebugCameraView = false; // Switch between main and debug camera view - bool debugIsPerspective = true; // Independent projection type for debug camera - bool debugIsLH = false; // Debug camera handedness defaults to RH (Camera lookat is RH) - bool firstFrame = true; - } interface; -}; - -NBL_MAIN_FUNC(UISampleApp) +NBL_MAIN_FUNC(App) diff --git a/61_UI/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp new file mode 100644 index 000000000..b0aa2b9a8 --- /dev/null +++ b/61_UI/src/keysmapping.cpp @@ -0,0 +1,244 @@ +#include "keysmapping.hpp" +#include "app/AppTypes.hpp" + +#include +#include + +inline std::string buildKeyCodeLabel(const ui::E_KEY_CODE keyCode) +{ + return std::string(1u, ui::keyCodeToChar(keyCode, true)); +} + +inline ImVec4 getBindingActiveStatusColor(const bool active) +{ + return active ? SCameraAppBindingEditorUiDefaults::ActiveStatusColor : SCameraAppBindingEditorUiDefaults::InactiveStatusColor; +} + +bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbalBindingLayout::BindingDomain activeBindingDomain, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +{ + bool anyMapUpdated = false; + ImGui::BeginTable(tableID, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); + ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, SCameraAppBindingEditorUiDefaults::TableColumnWeight); + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, SCameraAppBindingEditorUiDefaults::TableColumnWeight); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, SCameraAppBindingEditorUiDefaults::TableColumnWeight); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + if (ImGui::BeginCombo("##selectEvent", CVirtualGimbalEvent::virtualEventToString(selectedEventType).data())) + { + for (const auto& eventType : CVirtualGimbalEvent::VirtualEventsTypeTable) + { + bool isSelected = (selectedEventType == eventType); + if (ImGui::Selectable(CVirtualGimbalEvent::virtualEventToString(eventType).data(), isSelected)) + selectedEventType = eventType; + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + ImGui::TableSetColumnIndex(1); + if (activeBindingDomain == IGimbalBindingLayout::Keyboard) + { + const auto newKeyDisplay = buildKeyCodeLabel(newKey); + if (ImGui::BeginCombo("##selectKey", newKeyDisplay.c_str())) + { + for (int i = ui::E_KEY_CODE::EKC_A; i <= ui::E_KEY_CODE::EKC_Z; ++i) + { + bool isSelected = (newKey == static_cast(i)); + const auto label = buildKeyCodeLabel(static_cast(i)); + if (ImGui::Selectable(label.c_str(), isSelected)) + newKey = static_cast(i); + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + else + { + if (ImGui::BeginCombo("##selectMouseKey", ui::mouseCodeToString(newMouseCode).data())) + { + for (int i = ui::EMC_LEFT_BUTTON; i < ui::EMC_COUNT; ++i) + { + bool isSelected = (newMouseCode == static_cast(i)); + if (ImGui::Selectable(ui::mouseCodeToString(static_cast(i)).data(), isSelected)) + newMouseCode = static_cast(i); + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + + ImGui::TableSetColumnIndex(2); + if (ImGui::Button("Confirm Add", SCameraAppBindingEditorUiDefaults::ActionButtonSize)) + { + anyMapUpdated |= true; + if (activeBindingDomain == IGimbalBindingLayout::Keyboard) + layout->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); + else + layout->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); + addMode = false; + } + + ImGui::EndTable(); + + return anyMapUpdated; +} + +bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool spawnWindow) +{ + bool anyMapUpdated = false; + + if (!layout) return anyMapUpdated; + + struct MappingState + { + bool addMode = false; + CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; + ui::E_KEY_CODE newKey = ui::E_KEY_CODE::EKC_A; + ui::E_MOUSE_CODE newMouseCode = ui::EMC_LEFT_BUTTON; + IGimbalBindingLayout::BindingDomain activeBindingDomain = IGimbalBindingLayout::Keyboard; + }; + + static std::unordered_map cameraStates; + auto& state = cameraStates[layout]; + + const auto& keyboardMappings = layout->getKeyboardVirtualEventMap(); + const auto& mouseMappings = layout->getMouseVirtualEventMap(); + + if (spawnWindow) + { + ImGui::SetNextWindowSize(SCameraAppBindingEditorUiDefaults::WindowInitialSize, ImGuiCond_FirstUseEver); + ImGui::Begin("Binding Layouts & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + } + + if (ImGui::BeginTabBar("BindingsTabBar")) + { + if (ImGui::BeginTabItem("Keyboard")) + { + state.activeBindingDomain = IGimbalBindingLayout::Keyboard; + ImGui::Separator(); + + if (ImGui::Button("Add Key", SCameraAppBindingEditorUiDefaults::ActionButtonSize)) + state.addMode = !state.addMode; + + ImGui::Separator(); + + ImGui::BeginTable("KeyboardMappingsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); + ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Key(s)", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Active Status", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Magnitude", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableHeadersRow(); + + for (const auto& [keyboardCode, hash] : keyboardMappings) + { + ImGui::TableNextRow(); + const char* eventName = CVirtualGimbalEvent::virtualEventToString(hash.event.type).data(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", eventName); + + ImGui::TableSetColumnIndex(1); + const auto keyString = buildKeyCodeLabel(keyboardCode); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", keyString.c_str()); + + ImGui::TableSetColumnIndex(2); + bool isActive = (hash.event.magnitude > 0); + const ImVec4 statusColor = getBindingActiveStatusColor(isActive); + ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); + + ImGui::TableSetColumnIndex(3); + ImGui::Text("%.2f", hash.event.magnitude); + + ImGui::TableSetColumnIndex(4); + if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(keyboardCode))).c_str())) + { + anyMapUpdated |= true; + layout->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); + break; + } + } + ImGui::EndTable(); + + if (state.addMode) + { + ImGui::Separator(); + anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", layout, state.activeBindingDomain, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Mouse")) + { + state.activeBindingDomain = IGimbalBindingLayout::Mouse; + ImGui::Separator(); + + if (ImGui::Button("Add Key", SCameraAppBindingEditorUiDefaults::ActionButtonSize)) + state.addMode = !state.addMode; + + ImGui::Separator(); + + ImGui::BeginTable("MouseMappingsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); + ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Mouse Button(s)", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Active Status", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Magnitude", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableHeadersRow(); + + for (const auto& [mouseCode, hash] : mouseMappings) + { + ImGui::TableNextRow(); + const char* eventName = CVirtualGimbalEvent::virtualEventToString(hash.event.type).data(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", eventName); + + ImGui::TableSetColumnIndex(1); + const char* mouseButtonName = ui::mouseCodeToString(mouseCode).data(); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", mouseButtonName); + + ImGui::TableSetColumnIndex(2); + bool isActive = (hash.event.magnitude > 0); + const ImVec4 statusColor = getBindingActiveStatusColor(isActive); + ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); + + ImGui::TableSetColumnIndex(3); + ImGui::Text("%.2f", hash.event.magnitude); + + ImGui::TableSetColumnIndex(4); + if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(mouseCode))).c_str())) + { + anyMapUpdated |= true; + layout->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); + break; + } + } + ImGui::EndTable(); + + if (state.addMode) + { + ImGui::Separator(); + handleAddMapping("AddMouseMappingTable", layout, state.activeBindingDomain, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + } + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + if (spawnWindow) + ImGui::End(); + + return anyMapUpdated; +} + diff --git a/61_UI/src/transform.cpp b/61_UI/src/transform.cpp deleted file mode 100644 index e69de29bb..000000000 diff --git a/62_SchusslerTest/main.cpp b/62_SchusslerTest/main.cpp index 5407e194f..9d12f868d 100644 --- a/62_SchusslerTest/main.cpp +++ b/62_SchusslerTest/main.cpp @@ -8,7 +8,9 @@ #include #include "nbl/asset/utils/CGeometryCreator.h" -#include "CCamera.hpp" +#include +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" #include "../common/CommonAPI.h" using namespace nbl; @@ -80,7 +82,8 @@ class SchusslerTestApp : public ApplicationBase { CommonAPI::InputSystem::ChannelReader mouse; CommonAPI::InputSystem::ChannelReader keyboard; - Camera camera; + core::smart_refctd_ptr camera; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); int resourceIx; uint32_t acquiredNextFBO = {}; @@ -98,10 +101,10 @@ class SchusslerTestApp : public ApplicationBase { struct SPushConsts { struct VertStage { - core::matrix4SIMD VP; + hlsl::float32_t4x4 VP; } vertStage; struct FragStage { - core::vectorSIMDf campos; + hlsl::float32_t4 campos; BRDFTestNumber testNum; uint32_t pad[3]; } fragStage; @@ -319,12 +322,14 @@ class SchusslerTestApp : public ApplicationBase { renderFinished[i] = logicalDevice->createSemaphore(); } - matrix4SIMD projectionMatrix = - matrix4SIMD::buildProjectionMatrixPerspectiveFovLH( - core::radians(60.0f), float(WIN_W) / WIN_H, 0.01f, 5000.0f); - camera = Camera(core::vectorSIMDf(0.f, 0.f, 6.f), - core::vectorSIMDf(0.f, 0.f, -1.f), projectionMatrix, 10.f, - 1.f); + cameraProjection = hlsl::math::thin_lens::lhPerspectiveFovMatrix( + core::radians(60.0f), float(WIN_W) / WIN_H, 0.01f, 5000.0f); + camera = nbl::examples::CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(0.0, 0.0, 6.0), + hlsl::float64_t3(0.0, 0.0, -1.0), + {10.0, 1.0}); + if (!camera) + return logFail("Could not initialize camera orientation!"); } void workLoopBody() override { @@ -414,8 +419,10 @@ class SchusslerTestApp : public ApplicationBase { commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); SPushConsts pc; - pc.vertStage.VP = camera.getConcatenatedMatrix(); - pc.fragStage.campos = core::vectorSIMDf(&camera.getPosition().X); + pc.vertStage.VP = hlsl::math::linalg::promoted_mul( + cameraProjection, + hlsl::float32_t3x4(camera->getGimbal().getViewMatrix())); + pc.fragStage.campos = hlsl::float32_t4(hlsl::CCameraMathUtilities::castVector(camera->getGimbal().getPosition()), 1.0f); pc.fragStage.testNum = currentTestNum; commandBuffer->pushConstants( gpuGraphicsPipeline->getRenderpassIndependentPipeline()->getLayout(), @@ -446,4 +453,4 @@ class SchusslerTestApp : public ApplicationBase { void onAppTerminated_impl() override { logicalDevice->waitIdle(); } }; -NBL_COMMON_API_MAIN(SchusslerTestApp) \ No newline at end of file +NBL_COMMON_API_MAIN(SchusslerTestApp) diff --git a/67_RayQueryGeometry/include/common.hpp b/67_RayQueryGeometry/include/common.hpp index ac774b0df..b99af8ca2 100644 --- a/67_RayQueryGeometry/include/common.hpp +++ b/67_RayQueryGeometry/include/common.hpp @@ -2,6 +2,11 @@ #define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ #include "nbl/examples/examples.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" using namespace nbl; using namespace nbl::core; @@ -31,4 +36,4 @@ struct ReferenceObjectCpu } -#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ \ No newline at end of file +#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ diff --git a/67_RayQueryGeometry/main.cpp b/67_RayQueryGeometry/main.cpp index 63346ac4c..8b3a116b2 100644 --- a/67_RayQueryGeometry/main.cpp +++ b/67_RayQueryGeometry/main.cpp @@ -200,8 +200,15 @@ class RayQueryGeometryApp final : public SimpleWindowedApplication, public Built { core::vectorSIMDf cameraPosition(-5.81655884, 2.58630896, -4.23974705); core::vectorSIMDf cameraTarget(-0.349590302, -0.213266611, 0.317821503); - hlsl::float32_t4x4 projectionMatrix = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(60.0f), float(WIN_W) / WIN_H, 0.1f, 1000.0f); - camera = Camera(cameraPosition, cameraTarget, projectionMatrix, 1.069f, 0.4f); + cameraProjection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(60.0f), float(WIN_W) / WIN_H, 0.1f, 1000.0f); + camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(cameraPosition.x, cameraPosition.y, cameraPosition.z), + hlsl::float64_t3(cameraTarget.x, cameraTarget.y, cameraTarget.z), + {1.069, 0.4}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; } m_winMgr->show(m_window.get()); @@ -259,15 +266,24 @@ class RayQueryGeometryApp final : public SimpleWindowedApplication, public Built cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); cmdbuf->beginDebugMarker("RayQueryGeometryApp Frame"); { - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, m_logger.get()); - camera.endInputProcessing(nextPresentationTimestamp); + std::vector cameraMouseEvents; + std::vector cameraKeyboardEvents; + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + { + cameraMouseEvents.insert(cameraMouseEvents.end(), events.begin(), events.end()); + }, m_logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + cameraKeyboardEvents.insert(cameraKeyboardEvents.end(), events.begin(), events.end()); + }, m_logger.get()); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents(cameraMouseEvents, cameraKeyboardEvents, nextPresentationTimestamp, cameraInputRuntime, cameraInputConfig); + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); } - const auto viewMatrix = camera.getViewMatrix(); - const auto projectionMatrix = camera.getProjectionMatrix(); - const auto viewProjectionMatrix = camera.getConcatenatedMatrix(); + const auto viewMatrix = hlsl::float32_t3x4(camera->getGimbal().getViewMatrix()); + const auto projectionMatrix = cameraProjection; + const auto viewProjectionMatrix = hlsl::math::linalg::promoted_mul(cameraProjection, viewMatrix); hlsl::float32_t3x4 modelMatrix = hlsl::math::linalg::identity(); @@ -303,7 +319,8 @@ class RayQueryGeometryApp final : public SimpleWindowedApplication, public Built SPushConstants pc; pc.geometryInfoBuffer = geometryInfoBuffer->getDeviceAddress(); - const core::vector3df camPos = camera.getPosition().getAsVector3df(); + const auto camPos64 = camera->getGimbal().getPosition(); + const core::vector3df camPos(static_cast(camPos64.x), static_cast(camPos64.y), static_cast(camPos64.z)); pc.camPos = { camPos.X, camPos.Y, camPos.Z }; pc.invMVP = invModelViewProjectionMatrix; @@ -982,7 +999,11 @@ class RayQueryGeometryApp final : public SimpleWindowedApplication, public Built InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); video::CDumbPresentationOracle oracle; smart_refctd_ptr geometryInfoBuffer; @@ -995,4 +1016,4 @@ class RayQueryGeometryApp final : public SimpleWindowedApplication, public Built }; -NBL_MAIN_FUNC(RayQueryGeometryApp) \ No newline at end of file +NBL_MAIN_FUNC(RayQueryGeometryApp) diff --git a/70_FLIPFluids/main.cpp b/70_FLIPFluids/main.cpp index c702d512d..583215878 100644 --- a/70_FLIPFluids/main.cpp +++ b/70_FLIPFluids/main.cpp @@ -4,9 +4,13 @@ #include "nbl/this_example/builtin/build/spirv/keys.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" #include "nbl/examples/examples.hpp" // TODO: why is it not in nabla.h ? #include "nbl/asset/metadata/CHLSLMetadata.h" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" #include using namespace nbl; @@ -240,8 +244,15 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso float zNear = 0.1f, zFar = 10000.f; core::vectorSIMDf cameraPosition(14, 8, 12); core::vectorSIMDf cameraTarget(0, 0, 0); - hlsl::float32_t4x4 projectionMatrix = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(60.0f), float(WIN_WIDTH) / WIN_HEIGHT, zNear, zFar); - camera = Camera(cameraPosition, cameraTarget, projectionMatrix, 1.069f, 0.4f); + cameraProjection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(core::radians(60.0f), float(WIN_WIDTH) / WIN_HEIGHT, zNear, zFar); + camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(cameraPosition.x, cameraPosition.y, cameraPosition.z), + hlsl::float64_t3(cameraTarget.x, cameraTarget.y, cameraTarget.z), + {1.069, 0.4}); + if (!camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(cameraInputBinder, *camera); + cameraInputRuntime.binder = &cameraInputBinder; m_pRenderParams.zNear = zNear; m_pRenderParams.zFar = zFar; @@ -908,10 +919,20 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); cmdbuf->beginDebugMarker("Frame Debug FLIP sim begin"); { - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); mouseProcess(events); }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, m_logger.get()); - camera.endInputProcessing(nextPresentationTimestamp); + std::vector cameraMouseEvents; + std::vector cameraKeyboardEvents; + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + { + cameraMouseEvents.insert(cameraMouseEvents.end(), events.begin(), events.end()); + mouseProcess(events); + }, m_logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + cameraKeyboardEvents.insert(cameraKeyboardEvents.end(), events.begin(), events.end()); + }, m_logger.get()); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents(cameraMouseEvents, cameraKeyboardEvents, nextPresentationTimestamp, cameraInputRuntime, cameraInputConfig); + if (!virtualEvents.empty()) + camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); } // TODO: also need to protect from previous frame still reading while we overwrite UBO @@ -919,9 +940,9 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso SMVPParams camData; SBufferRange camDataRange; { - const auto viewMatrix = camera.getViewMatrix(); - const auto projectionMatrix = camera.getProjectionMatrix(); - const auto viewProjectionMatrix = camera.getConcatenatedMatrix(); + const auto viewMatrix = hlsl::float32_t3x4(camera->getGimbal().getViewMatrix()); + const auto projectionMatrix = cameraProjection; + const auto viewProjectionMatrix = hlsl::math::linalg::promoted_mul(cameraProjection, viewMatrix); hlsl::float32_t3x4 modelMatrix = hlsl::math::linalg::identity(); @@ -930,7 +951,8 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso auto modelMat = hlsl::math::linalg::promote_affine<4, 4, 3, 4>(modelMatrix); - const core::vector3df camPos = camera.getPosition().getAsVector3df(); + const auto camPos64 = camera->getGimbal().getPosition(); + const core::vector3df camPos(static_cast(camPos64.x), static_cast(camPos64.y), static_cast(camPos64.z)); camPos.getAs4Values(camData.cameraPosition); memcpy(camData.MVP, &modelViewProjectionMatrix[0][0], sizeof(camData.MVP)); @@ -1827,7 +1849,11 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; - Camera camera = Camera(core::vectorSIMDf(0,0,0), core::vectorSIMDf(0,0,0), hlsl::float32_t4x4()); + core::smart_refctd_ptr camera; + ui::CGimbalInputBinder cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig cameraInputConfig = {}; + hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); video::CDumbPresentationOracle oracle; bool m_shouldInitParticles = true; @@ -1881,4 +1907,4 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso smart_refctd_ptr tempAxisCellMaterialImageView; // uint4 }; -NBL_MAIN_FUNC(FLIPFluidsApp) \ No newline at end of file +NBL_MAIN_FUNC(FLIPFluidsApp) diff --git a/71_RayTracingPipeline/include/common.hpp b/71_RayTracingPipeline/include/common.hpp index e6b538618..b990a44d8 100644 --- a/71_RayTracingPipeline/include/common.hpp +++ b/71_RayTracingPipeline/include/common.hpp @@ -2,6 +2,11 @@ #define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ #include "nbl/examples/examples.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" using namespace nbl; using namespace nbl::core; diff --git a/71_RayTracingPipeline/main.cpp b/71_RayTracingPipeline/main.cpp index d18b85daf..d3b15b6a5 100644 --- a/71_RayTracingPipeline/main.cpp +++ b/71_RayTracingPipeline/main.cpp @@ -481,18 +481,11 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui [this]() -> void { ImGuiIO& io = ImGui::GetIO(); - m_camera.setProjectionMatrix([&]() - { - static hlsl::float32_t4x4 projection; - - projection = hlsl::math::thin_lens::rhPerspectiveFovMatrix( - core::radians(m_cameraSetting.fov), - io.DisplaySize.x / io.DisplaySize.y, - m_cameraSetting.zNear, - m_cameraSetting.zFar); - - return projection; - }()); + m_cameraProjection = hlsl::math::thin_lens::rhPerspectiveFovMatrix( + core::radians(m_cameraSetting.fov), + io.DisplaySize.x / io.DisplaySize.y, + m_cameraSetting.zNear, + m_cameraSetting.zFar); ImGui::SetNextWindowPos(ImVec2(1024, 100), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(256, 256), ImGuiCond_Appearing); @@ -555,15 +548,21 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui 0.01f, 500.0f ); - m_camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), proj); + m_cameraProjection = proj; + m_camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(cameraPosition.x, cameraPosition.y, cameraPosition.z), + hlsl::float64_t3(0.0, 0.0, 0.0), + {m_cameraSetting.moveSpeed, m_cameraSetting.rotateSpeed}); + if (!m_camera) + return logFail("Could not initialize camera orientation!"); + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(m_cameraInputBinder, *m_camera); + m_cameraInputRuntime.binder = &m_cameraInputBinder; } m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); m_surface->recreateSwapchain(); m_winMgr->show(m_window.get()); m_oracle.reportBeginFrameRecord(); - m_camera.mapKeysToWASD(); - return true; } @@ -623,9 +622,9 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); cmdbuf->beginDebugMarker("RaytracingPipelineApp Frame"); - const auto viewMatrix = m_camera.getViewMatrix(); - const auto projectionMatrix = m_camera.getProjectionMatrix(); - const auto viewProjectionMatrix = m_camera.getConcatenatedMatrix(); + const auto viewMatrix = hlsl::float32_t3x4(m_camera->getGimbal().getViewMatrixRH()); + const auto projectionMatrix = m_cameraProjection; + const auto viewProjectionMatrix = hlsl::math::linalg::promoted_mul(m_cameraProjection, viewMatrix); //hlsl::float32_t3x4 modelMatrix; @@ -667,7 +666,8 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui pc.proceduralGeomInfoBuffer = m_proceduralGeomInfoBuffer->getDeviceAddress(); pc.triangleGeomInfoBuffer = m_triangleGeomInfoBuffer->getDeviceAddress(); pc.frameCounter = m_frameAccumulationCounter; - const core::vector3df camPos = m_camera.getPosition().getAsVector3df(); + const auto camPos64 = m_camera->getGimbal().getPosition(); + const core::vector3df camPos(static_cast(camPos64.x), static_cast(camPos64.y), static_cast(camPos64.z)); pc.camPos = { camPos.X, camPos.Y, camPos.Z }; pc.invMVP = invModelViewProjectionMatrix; @@ -806,8 +806,7 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui inline void update() { - m_camera.setMoveSpeed(m_cameraSetting.moveSpeed); - m_camera.setRotateSpeed(m_cameraSetting.rotateSpeed); + CCameraSimpleFPSUtilities::applySpeedSettings(*m_camera, {m_cameraSetting.moveSpeed, m_cameraSetting.rotateSpeed}); static std::chrono::microseconds previousEventTimestamp{}; @@ -831,44 +830,44 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui { std::vector mouse{}; std::vector keyboard{}; + std::vector cameraMouse{}; + std::vector cameraKeyboard{}; } capturedEvents; - m_camera.beginInputProcessing(nextPresentationTimestamp); { const auto& io = ImGui::GetIO(); m_mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - if (!io.WantCaptureMouse) - m_camera.mouseProcess(events); // don't capture the events, only let camera handle them with its impl - - for (const auto& e : events) // here capture + for (const auto& e : events) { if (e.timeStamp < previousEventTimestamp) continue; previousEventTimestamp = e.timeStamp; capturedEvents.mouse.emplace_back(e); - + if (!io.WantCaptureMouse) + capturedEvents.cameraMouse.emplace_back(e); } }, m_logger.get()); m_keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { - if (!io.WantCaptureKeyboard) - m_camera.keyboardProcess(events); // don't capture the events, only let camera handle them with its impl - - for (const auto& e : events) // here capture + for (const auto& e : events) { if (e.timeStamp < previousEventTimestamp) continue; previousEventTimestamp = e.timeStamp; capturedEvents.keyboard.emplace_back(e); + if (!io.WantCaptureKeyboard) + capturedEvents.cameraKeyboard.emplace_back(e); } }, m_logger.get()); } - m_camera.endInputProcessing(nextPresentationTimestamp); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents(capturedEvents.cameraMouse, capturedEvents.cameraKeyboard, nextPresentationTimestamp, m_cameraInputRuntime, m_cameraInputConfig); + if (!virtualEvents.empty()) + m_camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); const core::SRange mouseEvents(capturedEvents.mouse.data(), capturedEvents.mouse.data() + capturedEvents.mouse.size()); const core::SRange keyboardEvents(capturedEvents.keyboard.data(), capturedEvents.keyboard.data() + capturedEvents.keyboard.size()); @@ -1463,7 +1462,11 @@ class RaytracingPipelineApp final : public SimpleWindowedApplication, public Bui float camXAngle = 32.f / 180.f * 3.14159f; } m_cameraSetting; - Camera m_camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); + core::smart_refctd_ptr m_camera; + ui::CGimbalInputBinder m_cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime m_cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig m_cameraInputConfig = {}; + hlsl::float32_t4x4 m_cameraProjection = hlsl::float32_t4x4(1.0f); Light m_light = { .direction = {-1.0f, -1.0f, -0.4f}, diff --git a/73_GeometryInspector/include/common.hpp b/73_GeometryInspector/include/common.hpp index cc06db2c1..8a5414998 100644 --- a/73_GeometryInspector/include/common.hpp +++ b/73_GeometryInspector/include/common.hpp @@ -3,6 +3,7 @@ #include "nbl/examples/examples.hpp" +#include "nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp" using namespace nbl; using namespace core; @@ -19,4 +20,4 @@ using namespace nbl::examples; #include "nbl/ext/ImGui/ImGui.h" #include "imgui/imgui_internal.h" -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file +#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/73_GeometryInspector/main.cpp b/73_GeometryInspector/main.cpp index 570ce52d2..519b39417 100644 --- a/73_GeometryInspector/main.cpp +++ b/73_GeometryInspector/main.cpp @@ -129,18 +129,11 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR [this]() -> void { ImGuiIO& io = ImGui::GetIO(); - m_camera.setProjectionMatrix([&]() - { - static hlsl::float32_t4x4 projection; - - projection = hlsl::math::thin_lens::rhPerspectiveFovMatrix( - core::radians(m_cameraSetting.fov), - io.DisplaySize.x / io.DisplaySize.y, - m_cameraSetting.zNear, - m_cameraSetting.zFar); - - return projection; - }()); + m_cameraProjection = hlsl::math::thin_lens::rhPerspectiveFovMatrix( + core::radians(m_cameraSetting.fov), + io.DisplaySize.x / io.DisplaySize.y, + m_cameraSetting.zNear, + m_cameraSetting.zFar); ImGuizmo::SetOrthographic(false); ImGuizmo::BeginFrame(); @@ -197,8 +190,8 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR auto& selectedInstance = m_renderer->getInstance(m_selectedMesh); - imguizmoM16InOut.view = hlsl::transpose(hlsl::math::linalg::promote_affine<4, 4, 3, 4>(m_camera.getViewMatrix())); - imguizmoM16InOut.projection = hlsl::transpose(m_camera.getProjectionMatrix()); + imguizmoM16InOut.view = hlsl::transpose(hlsl::math::linalg::promote_affine<4, 4, 3, 4>(hlsl::float32_t3x4(m_camera->getGimbal().getViewMatrixRH()))); + imguizmoM16InOut.projection = hlsl::transpose(m_cameraProjection); imguizmoM16InOut.projection[1][1] *= -1.f; // Flip y coordinates. https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ imguizmoM16InOut.model = hlsl::transpose(hlsl::math::linalg::promote_affine<4, 4, 3, 4>(selectedInstance.world)); { @@ -213,8 +206,6 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR if (!reloadModel()) return false; - m_camera.mapKeysToArrows(); - onAppInitializedFinish(); return true; } @@ -242,8 +233,7 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR inline void update(const std::chrono::microseconds nextPresentationTimestamp) { - m_camera.setMoveSpeed(m_cameraSetting.moveSpeed); - m_camera.setRotateSpeed(m_cameraSetting.rotateSpeed); + CCameraSimpleFPSUtilities::applySpeedSettings(*m_camera, {m_cameraSetting.moveSpeed, m_cameraSetting.rotateSpeed}); static std::chrono::microseconds previousEventTimestamp{}; @@ -254,34 +244,30 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR { std::vector mouse{}; std::vector keyboard{}; + std::vector cameraMouse{}; + std::vector cameraKeyboard{}; } capturedEvents; - m_camera.beginInputProcessing(nextPresentationTimestamp); { const auto& io = ImGui::GetIO(); + bool reload = false; m_mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - if (!io.WantCaptureMouse) - m_camera.mouseProcess(events); // don't capture the events, only let m_camera handle them with its impl - - for (const auto& e : events) // here capture + for (const auto& e : events) { if (e.timeStamp < previousEventTimestamp) continue; previousEventTimestamp = e.timeStamp; capturedEvents.mouse.emplace_back(e); - + if (!io.WantCaptureMouse) + capturedEvents.cameraMouse.emplace_back(e); } }, m_logger.get()); - bool reload = false; m_keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { - if (!io.WantCaptureKeyboard) - m_camera.keyboardProcess(events); // don't capture the events, only let m_camera handle them with its impl - - for (const auto& e : events) // here capture + for (const auto& e : events) { if (e.timeStamp < previousEventTimestamp) continue; @@ -290,12 +276,21 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR previousEventTimestamp = e.timeStamp; capturedEvents.keyboard.emplace_back(e); + if (!io.WantCaptureKeyboard) + capturedEvents.cameraKeyboard.emplace_back(e); } }, m_logger.get()); if (reload) reloadModel(); } - m_camera.endInputProcessing(nextPresentationTimestamp); + const auto virtualEvents = CCameraSimpleFPSUtilities::collectBasicVirtualEvents( + capturedEvents.cameraMouse, + capturedEvents.cameraKeyboard, + nextPresentationTimestamp, + m_cameraInputRuntime, + m_cameraInputConfig); + if (!virtualEvents.empty()) + m_camera->manipulate(std::span(virtualEvents.data(), virtualEvents.size())); const core::SRange mouseEvents(capturedEvents.mouse.data(), capturedEvents.mouse.data() + capturedEvents.mouse.size()); const core::SRange keyboardEvents(capturedEvents.keyboard.data(), capturedEvents.keyboard.data() + capturedEvents.keyboard.size()); @@ -357,8 +352,8 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR } // draw scene - float32_t3x4 viewMatrix = m_camera.getViewMatrix(); - float32_t4x4 viewProjMatrix = m_camera.getConcatenatedMatrix(); + float32_t3x4 viewMatrix = hlsl::float32_t3x4(m_camera->getGimbal().getViewMatrixRH()); + float32_t4x4 viewProjMatrix = hlsl::math::linalg::promoted_mul(m_cameraProjection, viewMatrix); m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); @@ -666,13 +661,19 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR { const auto measure = hlsl::length(diagonal); const auto aspectRatio = float(m_window->getWidth())/float(m_window->getHeight()); - m_camera.setProjectionMatrix(hlsl::math::thin_lens::rhPerspectiveFovMatrix(1.2f,aspectRatio,distance*measure*0.1f,measure*4.0f)); - m_camera.setMoveSpeed(measure*0.04); + m_cameraProjection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(1.2f,aspectRatio,distance*measure*0.1f,measure*4.0f); + m_cameraSetting.moveSpeed = measure*0.04f; } const auto pos = bound.maxVx+diagonal*distance; - m_camera.setPosition(vectorSIMDf(pos.x,pos.y,pos.z)); const auto center = (bound.minVx+bound.maxVx)*0.5f; - m_camera.setTarget(vectorSIMDf(center.x,center.y,center.z)); + m_camera = CCameraSimpleFPSUtilities::createFromLookAt( + hlsl::float64_t3(pos.x, pos.y, pos.z), + hlsl::float64_t3(center.x, center.y, center.z), + {m_cameraSetting.moveSpeed, m_cameraSetting.rotateSpeed}); + if (!m_camera) + return false; + ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(m_cameraInputBinder, *m_camera); + m_cameraInputRuntime.binder = &m_cameraInputBinder; } // TODO: write out the geometry @@ -714,7 +715,11 @@ class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinR float camXAngle = 32.f / 180.f * 3.14159f; } m_cameraSetting; - Camera m_camera = Camera(core::vectorSIMDf(0,0,0), core::vectorSIMDf(0,0,0), hlsl::float32_t4x4()); + core::smart_refctd_ptr m_camera; + ui::CGimbalInputBinder m_cameraInputBinder; + CCameraSimpleFPSUtilities::SBasicInputRuntime m_cameraInputRuntime = {}; + CCameraSimpleFPSUtilities::SBasicInputConfig m_cameraInputConfig = {}; + hlsl::float32_t4x4 m_cameraProjection = hlsl::float32_t4x4(1.0f); // mutables std::string m_modelPath; diff --git a/CMakeLists.txt b/CMakeLists.txt index 17c9c4999..fc2d3cc35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,7 +119,7 @@ if(NBL_BUILD_EXAMPLES) # we link common example api library and force examples to reuse its PCH foreach(T IN LISTS TARGETS) get_target_property(TYPE ${T} TYPE) - if(NOT ${TYPE} MATCHES INTERFACE) + if(NOT ${TYPE} MATCHES "INTERFACE|UTILITY") target_link_libraries(${T} PUBLIC ${NBL_EXAMPLES_API_TARGET}) target_include_directories(${T} PUBLIC $) set_target_properties(${T} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) @@ -134,4 +134,4 @@ if(NBL_BUILD_EXAMPLES) endforeach() NBL_ADJUST_FOLDERS(examples) -endif() \ No newline at end of file +endif() diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b91dfc91e..65d2b0ced 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -76,6 +76,7 @@ endif() add_subdirectory("src/nbl/examples" EXCLUDE_FROM_ALL) target_link_libraries(${LIB_NAME} PUBLIC NblExtExamplesAPISPIRV) +target_link_libraries(${LIB_NAME} PUBLIC Nabla::ext::Cameras) if(NBL_EMBED_BUILTIN_RESOURCES) INTERFACE_TO_BUILTINS(NblExtExamplesAPIBuiltinsBuild) @@ -106,4 +107,4 @@ set(NBL_EXAMPLES_API_TARGET ${LIB_NAME} PARENT_SCOPE) ]] set(NBL_EXAMPLES_API_LIBRARIES ${TARGETS} PARENT_SCOPE) -NBL_ADJUST_FOLDERS(common) \ No newline at end of file +NBL_ADJUST_FOLDERS(common) diff --git a/common/include/camera/CCameraControlPanelUiUtilities.hpp b/common/include/camera/CCameraControlPanelUiUtilities.hpp new file mode 100644 index 000000000..f77395537 --- /dev/null +++ b/common/include/camera/CCameraControlPanelUiUtilities.hpp @@ -0,0 +1,579 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_CONTROL_PANEL_UI_UTILITIES_HPP_ +#define _C_CAMERA_CONTROL_PANEL_UI_UTILITIES_HPP_ + +#include +#include +#include +#include +#include + +#include "imgui/imgui.h" +#include "imgui/misc/cpp/imgui_stdlib.h" + +namespace nbl::ui +{ + +/// @brief Shared visual theme and layout constants for the control panel consumer UI. +struct SCameraControlPanelStyle final +{ + static constexpr float MillisecondsPerSecond = 1000.0f; + static constexpr float WindowWidthRatio = 0.22f; + static constexpr float WindowMinWidth = 340.0f; + static constexpr float WindowMaxWidthRatio = 0.30f; + static constexpr float WindowHeightRatio = 0.32f; + static constexpr float WindowMinHeight = 200.0f; + static constexpr float WindowMaxHeightRatio = 0.50f; + + static constexpr ImVec2 WindowPadding = ImVec2(4.0f, 3.0f); + static constexpr ImVec2 FramePadding = ImVec2(3.0f, 1.0f); + static constexpr ImVec2 ItemSpacing = ImVec2(2.0f, 1.0f); + static constexpr ImVec2 CellPadding = ImVec2(2.0f, 1.0f); + static constexpr float WindowRounding = 4.0f; + static constexpr float FrameRounding = 3.0f; + static constexpr float TabRounding = 3.0f; + static constexpr float ScrollbarRounding = 4.0f; + static constexpr float WindowBorderSize = 1.0f; + + static constexpr ImVec4 WindowBgColor = ImVec4(0.05f, 0.06f, 0.08f, 0.0f); + static constexpr ImVec4 ChildBgColor = ImVec4(0.10f, 0.12f, 0.16f, 0.44f); + static constexpr ImVec4 BorderColor = ImVec4(0.64f, 0.72f, 0.84f, 0.55f); + static constexpr ImVec4 FrameBgColor = ImVec4(0.16f, 0.19f, 0.24f, 0.54f); + static constexpr ImVec4 FrameBgHoveredColor = ImVec4(0.26f, 0.32f, 0.40f, 0.64f); + static constexpr ImVec4 FrameBgActiveColor = ImVec4(0.30f, 0.36f, 0.45f, 0.70f); + static constexpr ImVec4 HeaderColor = ImVec4(0.14f, 0.18f, 0.24f, 0.60f); + static constexpr ImVec4 HeaderHoveredColor = ImVec4(0.24f, 0.30f, 0.40f, 0.70f); + static constexpr ImVec4 HeaderActiveColor = ImVec4(0.28f, 0.36f, 0.46f, 0.78f); + static constexpr ImVec4 TabColor = ImVec4(0.14f, 0.18f, 0.24f, 0.60f); + static constexpr ImVec4 TabHoveredColor = ImVec4(0.24f, 0.30f, 0.40f, 0.70f); + static constexpr ImVec4 TabActiveColor = ImVec4(0.20f, 0.26f, 0.36f, 0.78f); + static constexpr ImVec4 TableRowBgColor = ImVec4(0.12f, 0.14f, 0.18f, 0.50f); + static constexpr ImVec4 TableRowAltBgColor = ImVec4(0.16f, 0.18f, 0.22f, 0.50f); + static constexpr ImVec4 TextColor = ImVec4(0.98f, 0.99f, 1.0f, 1.0f); + static constexpr ImVec4 TextDisabledColor = ImVec4(0.82f, 0.86f, 0.90f, 1.0f); + static constexpr ImVec4 SeparatorColor = ImVec4(0.54f, 0.60f, 0.70f, 0.80f); + static constexpr ImVec4 SeparatorHoveredColor = ImVec4(0.68f, 0.76f, 0.88f, 0.90f); + static constexpr ImVec4 SeparatorActiveColor = ImVec4(0.82f, 0.90f, 1.0f, 0.96f); + + static constexpr ImVec4 AccentColor = ImVec4(0.60f, 0.82f, 1.0f, 1.0f); + static constexpr ImVec4 GoodColor = ImVec4(0.45f, 0.90f, 0.60f, 1.0f); + static constexpr ImVec4 BadColor = ImVec4(1.0f, 0.50f, 0.45f, 1.0f); + static constexpr ImVec4 WarnColor = ImVec4(0.95f, 0.80f, 0.45f, 1.0f); + static constexpr ImVec4 MutedColor = ImVec4(0.92f, 0.93f, 0.95f, 1.0f); + static constexpr ImVec4 BadgeTextColor = ImVec4(0.10f, 0.11f, 0.13f, 1.0f); + static constexpr ImVec4 KeyBackgroundColor = ImVec4(0.20f, 0.22f, 0.25f, 1.0f); + static constexpr ImVec4 KeyTextColor = ImVec4(0.92f, 0.94f, 0.96f, 1.0f); + static constexpr ImVec4 InactiveBadgeColor = ImVec4(0.35f, 0.36f, 0.38f, 1.0f); + + static constexpr ImVec4 PanelBackgroundColor = ImVec4(0.03f, 0.04f, 0.05f, 0.50f); + static constexpr ImVec4 PanelEdgeColor = ImVec4(0.62f, 0.70f, 0.84f, 0.60f); + static constexpr ImVec4 PanelStripeColor = ImVec4(0.28f, 0.56f, 0.90f, 0.70f); + static constexpr ImVec4 PanelShadowColor = ImVec4(0.0f, 0.0f, 0.0f, 0.12f); + static constexpr ImVec4 CardTopColor = ImVec4(0.20f, 0.22f, 0.26f, 0.98f); + static constexpr ImVec4 CardBottomColor = ImVec4(0.12f, 0.13f, 0.15f, 0.98f); + static constexpr ImVec4 CardBorderColor = ImVec4(0.45f, 0.48f, 0.54f, 1.0f); + static constexpr ImVec4 SectionChildBackgroundColor = ImVec4(0.14f, 0.18f, 0.22f, 0.52f); + static constexpr ImVec4 MiniStatChildBackgroundColor = ImVec4(0.14f, 0.16f, 0.19f, 0.75f); + + static constexpr ImVec2 BadgePadding = ImVec2(5.0f, 1.0f); + static constexpr ImVec2 KeyHintPadding = ImVec2(3.0f, 1.0f); + static constexpr float BadgeFramePaddingX = 5.0f; + static constexpr float BadgeFramePaddingY = 1.0f; + static constexpr float KeyHintFramePaddingX = 3.0f; + static constexpr float KeyHintFramePaddingY = 1.0f; + static constexpr float DotRadius = 3.0f; + static constexpr float DotYOffset = 1.0f; + static constexpr float DotSpacing = 5.0f; + static constexpr float SectionChildRounding = 4.0f; + static constexpr float CardChildRounding = 6.0f; + static constexpr ImVec2 CardWindowPadding = ImVec2(8.0f, 6.0f); + static constexpr float PanelShadowOffsetX = 2.0f; + static constexpr float PanelShadowOffsetY = 3.0f; + static constexpr float PanelShadowExtentX = 4.0f; + static constexpr float PanelShadowExtentY = 5.0f; + static constexpr float PanelStripeWidth = 4.0f; + static constexpr float PanelShadowRounding = 8.0f; + static constexpr float PanelRounding = 6.0f; + static constexpr float SectionHeaderWidth = 2.0f; + static constexpr float SectionHeaderTextOffsetX = 7.0f; + static constexpr float SectionHeaderHeight = 18.0f; + static constexpr float SectionSpacingY = 0.0f; + static constexpr float CardExtraRows = 0.7f; + static constexpr float CardHeightPadding = 6.0f; + static constexpr float MiniStatHeight = 48.0f; + static constexpr float MiniStatPlotHeight = 20.0f; + static constexpr float MiniStatChildRounding = 6.0f; + static constexpr float HeaderWindowHeight = 56.0f; + static constexpr float HeaderTitleFontScale = 1.04f; + static constexpr float HeaderMetricFontScale = 1.02f; + static constexpr float HeaderDummyY = 0.0f; + static constexpr float HeaderGapSmall = 1.0f; + static constexpr float TabChildRounding = 4.0f; + static constexpr ImVec2 TogglePadding = ImVec2(5.0f, 1.0f); + static constexpr float KeyframeListHeight = 108.0f; + static constexpr float EventLogBottomThreshold = 5.0f; + + static constexpr float DefaultFrameMetricMin = 16.0f; + static constexpr float DefaultEventMetricMin = 4.0f; + + static constexpr ImGuiTableFlags SummaryTableFlags = ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_PadOuterX; + static constexpr float SummaryLabelColumnWidth = 108.0f; +}; + +struct SCameraControlPanelBadgeData final +{ + const char* label = ""; + ImVec4 background = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); +}; + +struct SCameraControlPanelKeyHintGroup final +{ + const char* label = ""; + std::span keys = {}; +}; + +struct SCameraControlPanelMiniStatSpec final +{ + const char* id = ""; + const char* label = ""; + ImVec4 color = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + float minValue = 0.0f; +}; + +struct SCameraControlPanelCheckboxSpec final +{ + const char* label = ""; + bool* value = nullptr; + const char* hint = ""; +}; + +struct SCameraControlPanelSliderSpec final +{ + const char* label = ""; + float* value = nullptr; + float minValue = 0.0f; + float maxValue = 0.0f; + const char* format = "%.3f"; + ImGuiSliderFlags flags = ImGuiSliderFlags_None; + const char* hint = ""; +}; + +struct SCameraControlPanelStatusLineSpec final +{ + const char* label = ""; + std::string_view value = {}; + ImVec4 dotColor = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + ImVec4 valueColor = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); +}; + +struct SCameraControlPanelPolicyStatusSpec final +{ + const char* label = ""; + std::string_view value = {}; + bool active = false; +}; + +struct SCameraControlPanelHeaderHints final +{ + static inline constexpr std::array MoveKeys = { "W", "A", "S", "D" }; + static inline constexpr std::array LookKeys = { "RMB" }; + static inline constexpr std::array ZoomKeys = { "MW" }; +}; + +struct SCameraControlPanelToggleLabels final +{ + static inline constexpr std::array Labels = { "WINDOW", "STATUS", "EVENT LOG" }; +}; + +struct CCameraControlPanelUiUtilities final +{ + template + static inline void drawSummaryRow(const char* label, DrawValueFn&& drawValue) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + drawValue(); + } + + static inline void drawDot(const ImVec4& color, const SCameraControlPanelStyle& style = {}) + { + const ImVec2 cursor = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddCircleFilled( + ImVec2(cursor.x + style.DotRadius, cursor.y + style.DotRadius + style.DotYOffset), + style.DotRadius, + ImGui::ColorConvertFloat4ToU32(color)); + ImGui::Dummy(ImVec2(style.DotRadius * 2.0f + style.SectionHeaderWidth, style.DotRadius * 2.0f)); + ImGui::SameLine(0.0f, style.DotSpacing); + } + + static inline void drawStatusLine(const SCameraControlPanelStatusLineSpec& spec, const SCameraControlPanelStyle& style = {}) + { + drawSummaryRow(spec.label, [&]() + { + drawDot(spec.dotColor, style); + ImGui::TextColored(spec.valueColor, "%.*s", static_cast(spec.value.size()), spec.value.data()); + }); + } + + static inline void drawPolicyStatus(const SCameraControlPanelPolicyStatusSpec& spec, const SCameraControlPanelStyle& style = {}) + { + ImGui::TextDisabled("%s", spec.label); + ImGui::SameLine(); + ImGui::TextColored(spec.active ? style.GoodColor : style.BadColor, "%.*s", static_cast(spec.value.size()), spec.value.data()); + } + + static inline ImVec2 calcControlPanelWindowSize(const ImVec2& displaySize, const SCameraControlPanelStyle& style = {}) + { + return ImVec2( + std::clamp(displaySize.x * style.WindowWidthRatio, style.WindowMinWidth, displaySize.x * style.WindowMaxWidthRatio), + std::clamp(displaySize.y * style.WindowHeightRatio, style.WindowMinHeight, displaySize.y * style.WindowMaxHeightRatio)); + } + + static inline float calcFramesPerSecond(const float frameMs, const SCameraControlPanelStyle& style = {}) + { + return frameMs > 0.0f ? (style.MillisecondsPerSecond / frameMs) : 0.0f; + } + + static inline float calcPillWidth(const char* label, const ImVec2& padding) + { + return ImGui::CalcTextSize(label).x + padding.x * 2.0f; + } + + static inline void centerControlPanelRow(const float contentWidth) + { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - contentWidth) * 0.5f)); + } + + static inline void pushControlPanelWindowStyle(const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, style.FramePadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, style.WindowRounding); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.FrameRounding); + ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, style.TabRounding); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, style.ScrollbarRounding); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, style.WindowBorderSize); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, style.CellPadding); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, style.WindowBgColor); + ImGui::PushStyleColor(ImGuiCol_ChildBg, style.ChildBgColor); + ImGui::PushStyleColor(ImGuiCol_Border, style.BorderColor); + ImGui::PushStyleColor(ImGuiCol_FrameBg, style.FrameBgColor); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, style.FrameBgHoveredColor); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, style.FrameBgActiveColor); + ImGui::PushStyleColor(ImGuiCol_Header, style.HeaderColor); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, style.HeaderHoveredColor); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, style.HeaderActiveColor); + ImGui::PushStyleColor(ImGuiCol_Tab, style.TabColor); + ImGui::PushStyleColor(ImGuiCol_TabHovered, style.TabHoveredColor); + ImGui::PushStyleColor(ImGuiCol_TabActive, style.TabActiveColor); + ImGui::PushStyleColor(ImGuiCol_TableRowBg, style.TableRowBgColor); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, style.TableRowAltBgColor); + ImGui::PushStyleColor(ImGuiCol_Text, style.TextColor); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, style.TextDisabledColor); + ImGui::PushStyleColor(ImGuiCol_Separator, style.SeparatorColor); + ImGui::PushStyleColor(ImGuiCol_SeparatorHovered, style.SeparatorHoveredColor); + ImGui::PushStyleColor(ImGuiCol_SeparatorActive, style.SeparatorActiveColor); + } + + static inline void popControlPanelWindowStyle() + { + ImGui::PopStyleColor(19); + ImGui::PopStyleVar(9); + } + + static inline bool inputTextString( + const char* label, + std::string& value, + ImGuiInputTextFlags flags = 0) + { + return ImGui::InputText(label, &value, flags); + } + + static inline void drawControlPanelWindowBackdrop(ImDrawList& drawList, const ImVec2& panelPos, const ImVec2& panelSize, const SCameraControlPanelStyle& style = {}) + { + const ImVec2 panelMax(panelPos.x + panelSize.x, panelPos.y + panelSize.y); + drawList.AddRectFilled( + ImVec2(panelPos.x + style.PanelShadowOffsetX, panelPos.y + style.PanelShadowOffsetY), + ImVec2(panelPos.x + panelSize.x + style.PanelShadowExtentX, panelPos.y + panelSize.y + style.PanelShadowExtentY), + ImGui::ColorConvertFloat4ToU32(style.PanelShadowColor), + style.PanelShadowRounding); + drawList.AddRectFilled(panelPos, panelMax, ImGui::ColorConvertFloat4ToU32(style.PanelBackgroundColor), style.PanelRounding); + drawList.AddRect(panelPos, panelMax, ImGui::ColorConvertFloat4ToU32(style.PanelEdgeColor), style.PanelRounding); + drawList.AddRectFilled( + panelPos, + ImVec2(panelPos.x + style.PanelStripeWidth, panelPos.y + panelSize.y), + ImGui::ColorConvertFloat4ToU32(style.PanelStripeColor), + style.PanelRounding); + } + + static inline float calcCameraControlPanelCardHeight(const int rows, const SCameraControlPanelStyle& style = {}) + { + return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + style.CardExtraRows) + style.CardHeightPadding; + } + + static inline void drawBadge(const char* label, const ImVec4& bg, const ImVec4& fg, const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleColor(ImGuiCol_Button, bg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, bg); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, bg); + ImGui::PushStyleColor(ImGuiCol_Text, fg); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(style.BadgeFramePaddingX, style.BadgeFramePaddingY)); + ImGui::Button(label); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + static inline float calcBadgeRowWidth( + const std::span badges, + const float gap, + const ImVec2& badgePadding) + { + float width = 0.0f; + for (size_t i = 0; i < badges.size(); ++i) + { + if (i > 0u) + width += gap; + width += calcPillWidth(badges[i].label, badgePadding); + } + return width; + } + + static inline void drawBadgeRow( + const std::span badges, + const ImVec4& textColor, + const float gap, + const SCameraControlPanelStyle& style = {}) + { + if (badges.empty()) + return; + + centerControlPanelRow(calcBadgeRowWidth(badges, gap, style.BadgePadding)); + for (size_t i = 0; i < badges.size(); ++i) + { + if (i > 0u) + ImGui::SameLine(0.0f, gap); + drawBadge(badges[i].label, badges[i].background, textColor, style); + } + } + + static inline void drawKeyHint(const char* label, const ImVec4& bg, const ImVec4& fg, const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleColor(ImGuiCol_Button, bg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, bg); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, bg); + ImGui::PushStyleColor(ImGuiCol_Text, fg); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(style.KeyHintFramePaddingX, style.KeyHintFramePaddingY)); + ImGui::SmallButton(label); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + static inline float calcKeyHintGroupWidth( + const SCameraControlPanelKeyHintGroup& group, + const float gap, + const ImVec2& keyPadding) + { + float width = ImGui::CalcTextSize(group.label).x; + for (const char* key : group.keys) + width += gap + calcPillWidth(key, keyPadding); + return width; + } + + static inline void drawKeyHintGroup( + const SCameraControlPanelKeyHintGroup& group, + const float gap, + const ImVec4& keyBackground, + const ImVec4& keyText, + const SCameraControlPanelStyle& style = {}) + { + ImGui::TextDisabled("%s", group.label); + for (const char* key : group.keys) + { + ImGui::SameLine(0.0f, gap); + drawKeyHint(key, keyBackground, keyText, style); + } + } + + static inline void drawKeyHintGroupRow( + const std::span groups, + const float gap, + const float groupGap, + const ImVec4& keyBackground, + const ImVec4& keyText, + const SCameraControlPanelStyle& style = {}) + { + float rowWidth = 0.0f; + for (size_t i = 0; i < groups.size(); ++i) + { + if (i > 0u) + rowWidth += groupGap; + rowWidth += calcKeyHintGroupWidth(groups[i], gap, style.KeyHintPadding); + } + + centerControlPanelRow(rowWidth); + for (size_t i = 0; i < groups.size(); ++i) + { + if (i > 0u) + ImGui::SameLine(0.0f, groupGap); + drawKeyHintGroup(groups[i], gap, keyBackground, keyText, style); + } + } + + static inline void drawTogglePill( + const char* label, + bool& value, + const ImVec4& onColor, + const ImVec4& offColor, + const ImVec4& textColor, + const ImVec2& padding) + { + ImGui::PushStyleColor(ImGuiCol_Button, value ? onColor : offColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, value ? onColor : offColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, value ? onColor : offColor); + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, padding); + if (ImGui::Button(label)) + value = !value; + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + template + static inline void drawMiniStat( + const SCameraControlPanelMiniStatSpec& stat, + const std::array& series, + const size_t metricIndex, + DrawValueFn&& drawValue, + const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleColor(ImGuiCol_ChildBg, style.MiniStatChildBackgroundColor); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.MiniStatChildRounding); + if (ImGui::BeginChild(stat.id, ImVec2(0.0f, style.MiniStatHeight), true, ImGuiWindowFlags_NoScrollbar)) + { + ImGui::TextDisabled("%s", stat.label); + ImGui::SetWindowFontScale(style.HeaderMetricFontScale); + drawValue(); + ImGui::SetWindowFontScale(1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotLines, stat.color); + float maxValue = stat.minValue; + for (const float value : series) + maxValue = std::max(maxValue, value); + ImGui::PlotLines("##plot", series.data(), static_cast(SampleCount), static_cast(metricIndex), nullptr, 0.0f, maxValue, ImVec2(0.0f, style.MiniStatPlotHeight)); + ImGui::PopStyleColor(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + + static inline void drawHoverHint(const char* text) + { + if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) + return; + ImGui::BeginTooltip(); + ImGui::TextUnformatted(text); + ImGui::EndTooltip(); + } + + static inline bool drawCheckboxWithHint(const SCameraControlPanelCheckboxSpec& spec) + { + if (!spec.value) + return false; + + const bool changed = ImGui::Checkbox(spec.label, spec.value); + if (spec.hint && spec.hint[0] != '\0') + drawHoverHint(spec.hint); + return changed; + } + + static inline bool drawSliderFloatWithHint(const SCameraControlPanelSliderSpec& spec) + { + if (!spec.value) + return false; + + const bool changed = ImGui::SliderFloat(spec.label, spec.value, spec.minValue, spec.maxValue, spec.format, spec.flags); + if (spec.hint && spec.hint[0] != '\0') + drawHoverHint(spec.hint); + return changed; + } + + static inline bool drawActionButtonWithHint(const char* label, const char* hint) + { + const bool pressed = ImGui::Button(label); + if (hint && hint[0] != '\0') + drawHoverHint(hint); + return pressed; + } + + static inline bool beginControlPanelTabChild(const char* id, const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.TabChildRounding); + return ImGui::BeginChild(id, ImVec2(0.0f, 0.0f), true); + } + + static inline void endControlPanelTabChild() + { + ImGui::EndChild(); + ImGui::PopStyleVar(); + } + + static inline void drawSectionHeader(const char* id, const char* label, const ImVec4& accent, const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.SectionChildRounding); + ImGui::PushStyleColor(ImGuiCol_ChildBg, style.SectionChildBackgroundColor); + if (ImGui::BeginChild(id, ImVec2(0.0f, style.SectionHeaderHeight), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + const ImVec2 pos = ImGui::GetWindowPos(); + const ImVec2 size = ImGui::GetWindowSize(); + ImGui::GetWindowDrawList()->AddRectFilled( + pos, + ImVec2(pos.x + style.SectionHeaderWidth, pos.y + size.y), + ImGui::ColorConvertFloat4ToU32(accent), + style.SectionChildRounding); + ImGui::SetCursorPosX(style.SectionHeaderTextOffsetX); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(accent, "%s", label); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::Spacing(); + } + + static inline bool beginCard(const char* id, const float height, const ImVec4& top, const ImVec4& bottom, const ImVec4& border, const SCameraControlPanelStyle& style = {}) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.CardChildRounding); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.CardWindowPadding); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + const bool open = ImGui::BeginChild(id, ImVec2(0.0f, height), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + const ImVec2 pos = ImGui::GetWindowPos(); + const ImVec2 size = ImGui::GetWindowSize(); + ImGui::GetWindowDrawList()->AddRectFilledMultiColor( + pos, + ImVec2(pos.x + size.x, pos.y + size.y), + ImGui::ColorConvertFloat4ToU32(top), + ImGui::ColorConvertFloat4ToU32(top), + ImGui::ColorConvertFloat4ToU32(bottom), + ImGui::ColorConvertFloat4ToU32(bottom)); + ImGui::GetWindowDrawList()->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), ImGui::ColorConvertFloat4ToU32(border), style.CardChildRounding); + return open; + } + + static inline void endCard() + { + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); + } +}; + +} // namespace nbl::ui + +#endif // _C_CAMERA_CONTROL_PANEL_UI_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp new file mode 100644 index 000000000..bad69f7ad --- /dev/null +++ b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp @@ -0,0 +1,208 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SCRIPT_VISUAL_DEBUG_OVERLAY_UTILITIES_HPP_ +#define _C_CAMERA_SCRIPT_VISUAL_DEBUG_OVERLAY_UTILITIES_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "imgui/imgui.h" + +namespace nbl::ui +{ + +/// @brief Shared data bundle for the scripted visual debug HUD. +struct SCameraScriptVisualDebugOverlayData final +{ + std::string title; + std::string headline; + std::string progressLine; + std::string hintLine; + + inline bool valid() const + { + return !headline.empty() && !progressLine.empty(); + } +}; + +/// @brief Shared camera/debug state used to format one scripted visual debug HUD payload. +struct SCameraScriptVisualDebugStatus final +{ + static constexpr float DefaultTargetFps = 60.0f; + std::string_view title = "SCRIPT VISUAL DEBUG"; + std::string_view cameraLabel = "Unknown"; + std::string_view cameraHint = "Unspecified camera behavior"; + uint32_t cameraIndex = 0u; + uint32_t cameraCount = 0u; + uint32_t planarIndex = 0u; + bool hasHoldFrames = false; + uint64_t progressFrames = 0u; + uint64_t holdFrames = 0u; + float targetFps = DefaultTargetFps; + uint64_t absoluteFrame = 0u; + std::string_view segmentLabel = {}; + bool hasDynamicFov = false; + float dynamicFovDeg = 0.0f; + bool followActive = false; + std::string_view followModeDescription = "Follow off"; + bool followLockValid = false; + float followLockAngleDeg = 0.0f; + float followTargetDistance = 0.0f; + float followTargetCenterNdcRadius = 0.0f; +}; + +/// @brief Shared style bundle for the scripted visual debug HUD. +struct SCameraScriptVisualDebugOverlayStyle final +{ + static constexpr float TitleSize = 50.0f; + static constexpr float HeadlineSize = 38.0f; + static constexpr float ProgressSize = 28.0f; + static constexpr float HintSize = 24.0f; + static constexpr float MarginTop = 18.0f; + static constexpr float PaddingX = 24.0f; + static constexpr float PaddingY = 16.0f; + static constexpr float LineGap = 6.0f; + static constexpr float CornerRounding = 14.0f; + static constexpr float BorderThickness = 2.5f; + static constexpr ImU32 BackgroundColor = IM_COL32(6, 8, 12, 232); + static constexpr ImU32 BorderColor = IM_COL32(255, 166, 64, 255); + static constexpr ImU32 TitleColor = IM_COL32(255, 206, 120, 255); + static constexpr ImU32 HeadlineColor = IM_COL32(255, 244, 224, 255); + static constexpr ImU32 ProgressColor = IM_COL32(202, 222, 255, 255); + static constexpr ImU32 HintColor = IM_COL32(170, 204, 255, 255); + + float titleSize = TitleSize; + float headlineSize = HeadlineSize; + float progressSize = ProgressSize; + float hintSize = HintSize; + float marginTop = MarginTop; + float paddingX = PaddingX; + float paddingY = PaddingY; + float lineGap = LineGap; + float cornerRounding = CornerRounding; + float borderThickness = BorderThickness; + ImU32 backgroundColor = BackgroundColor; + ImU32 borderColor = BorderColor; + ImU32 titleColor = TitleColor; + ImU32 headlineColor = HeadlineColor; + ImU32 progressColor = ProgressColor; + ImU32 hintColor = HintColor; +}; + +struct CCameraScriptVisualDebugOverlayUtilities final +{ + static inline void drawScriptVisualDebugOverlay( + const ImVec2& displaySize, + const SCameraScriptVisualDebugOverlayData& data, + const SCameraScriptVisualDebugOverlayStyle& style = {}) + { + if (!data.valid()) + return; + + ImFont* font = ImGui::GetFont(); + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + if (!font || !drawList) + return; + + const float textWrap = std::numeric_limits::max(); + const ImVec2 titleSize = font->CalcTextSizeA(style.titleSize, textWrap, 0.0f, data.title.c_str()); + const ImVec2 headlineSize = font->CalcTextSizeA(style.headlineSize, textWrap, 0.0f, data.headline.c_str()); + const ImVec2 progressSize = font->CalcTextSizeA(style.progressSize, textWrap, 0.0f, data.progressLine.c_str()); + const ImVec2 hintSize = font->CalcTextSizeA(style.hintSize, textWrap, 0.0f, data.hintLine.c_str()); + const float panelWidth = std::max(std::max(titleSize.x, headlineSize.x), std::max(progressSize.x, hintSize.x)) + style.paddingX * 2.0f; + const float panelHeight = titleSize.y + headlineSize.y + progressSize.y + hintSize.y + style.lineGap * 3.0f + style.paddingY * 2.0f; + const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, style.marginTop); + const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); + + drawList->AddRectFilled(panelMin, panelMax, style.backgroundColor, style.cornerRounding); + drawList->AddRect(panelMin, panelMax, style.borderColor, style.cornerRounding, 0, style.borderThickness); + + const float titleX = panelMin.x + (panelWidth - titleSize.x) * 0.5f; + const float headlineX = panelMin.x + (panelWidth - headlineSize.x) * 0.5f; + const float progressX = panelMin.x + (panelWidth - progressSize.x) * 0.5f; + const float hintX = panelMin.x + (panelWidth - hintSize.x) * 0.5f; + const float titleY = panelMin.y + style.paddingY; + const float headlineY = titleY + titleSize.y + style.lineGap; + const float progressY = headlineY + headlineSize.y + style.lineGap; + const float hintY = progressY + progressSize.y + style.lineGap; + + drawList->AddText(font, style.titleSize, ImVec2(titleX, titleY), style.titleColor, data.title.c_str()); + drawList->AddText(font, style.headlineSize, ImVec2(headlineX, headlineY), style.headlineColor, data.headline.c_str()); + drawList->AddText(font, style.progressSize, ImVec2(progressX, progressY), style.progressColor, data.progressLine.c_str()); + drawList->AddText(font, style.hintSize, ImVec2(hintX, hintY), style.hintColor, data.hintLine.c_str()); + } + + static inline std::string formatFixedScalar(const float value, const int precision) + { + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision) << value; + return oss.str(); + } + + static inline std::string buildScriptVisualDebugProgressLine(const SCameraScriptVisualDebugStatus& status) + { + if (status.hasHoldFrames) + { + const float safeFps = std::max(status.targetFps, 1.0f); + const double elapsedSeconds = static_cast(status.progressFrames) / static_cast(safeFps); + const double holdSeconds = static_cast(status.holdFrames) / static_cast(safeFps); + + std::ostringstream oss; + oss << "Planar " << status.planarIndex + << " Segment " << std::fixed << std::setprecision(1) + << elapsedSeconds << "/" << holdSeconds + << " s Frame " << status.progressFrames << "/" << status.holdFrames; + return oss.str(); + } + + std::ostringstream oss; + oss << "Planar " << status.planarIndex << " Frame " << status.absoluteFrame; + return oss.str(); + } + + static inline SCameraScriptVisualDebugOverlayData buildScriptVisualDebugOverlayData(const SCameraScriptVisualDebugStatus& status) + { + SCameraScriptVisualDebugOverlayData out = {}; + out.title = std::string(status.title); + out.headline = "Camera " + std::to_string(status.cameraIndex + 1u) + "/" + std::to_string(status.cameraCount) + " " + std::string(status.cameraLabel); + out.progressLine = buildScriptVisualDebugProgressLine(status); + if (!status.segmentLabel.empty()) + out.progressLine += " | " + std::string(status.segmentLabel); + + out.hintLine = std::string(status.cameraHint); + if (status.hasDynamicFov) + out.hintLine += " | Dynamic FOV " + formatFixedScalar(status.dynamicFovDeg, 2) + " deg"; + + if (status.followActive) + { + out.hintLine += " | " + std::string(status.followModeDescription); + if (status.followLockValid) + { + out.hintLine += + " | lock " + formatFixedScalar(status.followLockAngleDeg, 2) + + " deg | target " + formatFixedScalar(status.followTargetDistance, 2) + + " | center err " + formatFixedScalar(status.followTargetCenterNdcRadius, 3); + } + else + { + out.hintLine += " | lock n/a | target n/a | center err n/a"; + } + } + else + { + out.hintLine += " | Follow off"; + } + + return out; + } +}; + +} // namespace nbl::ui + +#endif // _C_CAMERA_SCRIPT_VISUAL_DEBUG_OVERLAY_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp new file mode 100644 index 000000000..a1c2d364a --- /dev/null +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -0,0 +1,317 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_VIEWPORT_OVERLAY_UTILITIES_HPP_ +#define _C_CAMERA_VIEWPORT_OVERLAY_UTILITIES_HPP_ + +#include +#include + +#include "nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp" +#include "imgui/imgui.h" + +namespace nbl::ui +{ + +/// @brief Screen-space viewport rectangle used by debug overlay helpers. +struct SViewportOverlayRect final +{ + ImVec2 position = ImVec2(0.0f, 0.0f); + ImVec2 size = ImVec2(0.0f, 0.0f); + + inline bool valid() const + { + return size.x > 1.0f && size.y > 1.0f; + } + + inline ImVec2 getCenter() const + { + return ImVec2(position.x + size.x * 0.5f, position.y + size.y * 0.5f); + } + + inline ImVec2 ndcToScreen(const ImVec2& ndcPoint) const + { + return ImVec2( + position.x + (ndcPoint.x * 0.5f + 0.5f) * size.x, + position.y + (-ndcPoint.y * 0.5f + 0.5f) * size.y); + } +}; + +/// @brief Shared style bundle for the follow-target viewport overlay. +struct SCameraFollowTargetViewportOverlayStyle final +{ + static constexpr float CenteredNdcRadius = system::SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance; + static constexpr float CenterRadius = 16.0f; + static constexpr float CenterCrossHalfExtent = 22.0f; + static constexpr float CenterLineThickness = 2.0f; + static constexpr float CenterCircleThickness = 2.5f; + static constexpr float CenteredTargetRadius = 18.0f; + static constexpr float DefaultTargetRadius = 14.0f; + static constexpr float TargetCrossHalfExtent = 14.0f; + static constexpr float LinkLineThickness = 2.0f; + static constexpr float LabelOffsetX = 16.0f; + static constexpr float LabelOffsetY = -28.0f; + static constexpr int32_t CircleSegments = 32; + static constexpr int32_t FilledCircleSegments = 24; + static constexpr ImU32 CenterColor = IM_COL32(255, 170, 72, 235); + static constexpr ImU32 CenteredTargetColor = IM_COL32(64, 255, 164, 245); + static constexpr ImU32 DefaultTargetColor = IM_COL32(90, 220, 255, 245); + static constexpr ImU32 CenteredTargetFillColor = IM_COL32(24, 120, 76, 120); + static constexpr ImU32 DefaultTargetFillColor = IM_COL32(20, 92, 124, 120); + static constexpr ImU32 CenteredLinkColor = IM_COL32(96, 255, 186, 200); + static constexpr ImU32 DefaultLinkColor = IM_COL32(120, 220, 255, 200); + + float centeredNdcRadius = CenteredNdcRadius; + float centerRadius = CenterRadius; + float centerCrossHalfExtent = CenterCrossHalfExtent; + float centerLineThickness = CenterLineThickness; + float centerCircleThickness = CenterCircleThickness; + float centeredTargetRadius = CenteredTargetRadius; + float defaultTargetRadius = DefaultTargetRadius; + float targetCrossHalfExtent = TargetCrossHalfExtent; + float linkLineThickness = LinkLineThickness; + float labelOffsetX = LabelOffsetX; + float labelOffsetY = LabelOffsetY; + int32_t circleSegments = CircleSegments; + int32_t filledCircleSegments = FilledCircleSegments; + ImU32 centerColor = CenterColor; + ImU32 centeredTargetColor = CenteredTargetColor; + ImU32 defaultTargetColor = DefaultTargetColor; + ImU32 centeredTargetFillColor = CenteredTargetFillColor; + ImU32 defaultTargetFillColor = DefaultTargetFillColor; + ImU32 centeredLinkColor = CenteredLinkColor; + ImU32 defaultLinkColor = DefaultLinkColor; +}; + +/// @brief Shared visual style for transparent viewport windows used by scene image panels. +struct SCameraViewportWindowStyle final +{ + static constexpr float WindowRounding = 0.0f; + static constexpr float WindowBorderSize = 0.0f; + static constexpr ImVec2 WindowPadding = ImVec2(0.0f, 0.0f); + static constexpr ImVec4 WindowBackgroundColor = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + + float windowRounding = WindowRounding; + float windowBorderSize = WindowBorderSize; + ImVec2 windowPadding = WindowPadding; + ImVec4 windowBackgroundColor = WindowBackgroundColor; +}; + +/// @brief Shared data bundle for the top-right camera/projection overlay rendered inside one viewport window. +struct SCameraViewportInfoOverlayData final +{ + std::string headline; + std::string description; + std::string detail; + + inline bool valid() const + { + return !headline.empty() && !description.empty(); + } +}; + +/// @brief Shared style bundle for the top-right camera/projection overlay rendered inside one viewport window. +struct SCameraViewportInfoOverlayStyle final +{ + static constexpr ImVec2 Padding = ImVec2(6.0f, 4.0f); + static constexpr float LineGap = 2.0f; + static constexpr float Margin = 6.0f; + static constexpr float CornerRounding = 6.0f; + static constexpr ImU32 BackgroundColor = IM_COL32(13, 15, 20, 204); + static constexpr ImU32 BorderColor = IM_COL32(153, 168, 194, 204); + static constexpr ImU32 HeadlineColor = IM_COL32(245, 250, 255, 255); + static constexpr ImU32 DescriptionColor = IM_COL32(199, 209, 230, 255); + static constexpr ImU32 DetailColor = IM_COL32(245, 230, 92, 255); + + ImVec2 padding = Padding; + float lineGap = LineGap; + float margin = Margin; + float cornerRounding = CornerRounding; + ImU32 backgroundColor = BackgroundColor; + ImU32 borderColor = BorderColor; + ImU32 headlineColor = HeadlineColor; + ImU32 descriptionColor = DescriptionColor; + ImU32 detailColor = DetailColor; +}; + +/// @brief Shared style bundle for small hover-info popups near the mouse cursor. +struct SCameraHoverInfoOverlayStyle final +{ + static constexpr ImVec4 WindowBackgroundColor = ImVec4(0.20f, 0.20f, 0.20f, 0.80f); + static constexpr ImVec4 BorderColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + static constexpr float BorderSize = 1.5f; + static constexpr ImVec2 MouseOffset = ImVec2(10.0f, 10.0f); + + ImVec4 windowBackgroundColor = WindowBackgroundColor; + ImVec4 borderColor = BorderColor; + float borderSize = BorderSize; + ImVec2 mouseOffset = MouseOffset; +}; + +/// @brief Shared style bundle for the split divider overlay between stacked viewport windows. +struct SCameraViewportSplitOverlayStyle final +{ + static constexpr float MinimumGapFill = 2.0f; + static constexpr float DividerLineThickness = 2.0f; + static constexpr ImU32 GapFillColor = IM_COL32(13, 15, 20, 217); + static constexpr ImU32 DividerLineColor = IM_COL32(204, 214, 235, 191); + + float minimumGapFill = MinimumGapFill; + float dividerLineThickness = DividerLineThickness; + ImU32 gapFillColor = GapFillColor; + ImU32 dividerLineColor = DividerLineColor; +}; + +struct CCameraViewportOverlayUtilities final +{ + static inline void pushViewportWindowStyle(const SCameraViewportWindowStyle& style = {}) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, style.windowRounding); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, style.windowBorderSize); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.windowPadding); + ImGui::PushStyleColor(ImGuiCol_WindowBg, style.windowBackgroundColor); + } + + static inline void popViewportWindowStyle() + { + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); + } + + static inline void drawViewportInfoOverlay( + ImDrawList& drawList, + const SViewportOverlayRect& viewportRect, + const SCameraViewportInfoOverlayData& data, + const SCameraViewportInfoOverlayStyle& style = {}) + { + if (!viewportRect.valid() || !data.valid()) + return; + + const ImVec2 headlineSize = ImGui::CalcTextSize(data.headline.c_str()); + const ImVec2 descriptionSize = ImGui::CalcTextSize(data.description.c_str()); + const bool hasDetail = !data.detail.empty(); + const ImVec2 detailSize = hasDetail ? ImGui::CalcTextSize(data.detail.c_str()) : ImVec2(0.0f, 0.0f); + const float width = std::max(std::max(headlineSize.x, descriptionSize.x), detailSize.x); + float height = headlineSize.y + descriptionSize.y + style.lineGap + style.padding.y * 2.0f; + if (hasDetail) + height += detailSize.y + style.lineGap; + ImVec2 overlayPos( + viewportRect.position.x + viewportRect.size.x - width - style.padding.x * 2.0f - style.margin, + viewportRect.position.y + style.margin); + overlayPos.x = std::max(overlayPos.x, viewportRect.position.x + style.margin); + const ImVec2 overlayMax(overlayPos.x + width + style.padding.x * 2.0f, overlayPos.y + height); + + drawList.AddRectFilled(overlayPos, overlayMax, style.backgroundColor, style.cornerRounding); + drawList.AddRect(overlayPos, overlayMax, style.borderColor, style.cornerRounding); + drawList.AddText(ImVec2(overlayPos.x + style.padding.x, overlayPos.y + style.padding.y), style.headlineColor, data.headline.c_str()); + drawList.AddText( + ImVec2(overlayPos.x + style.padding.x, overlayPos.y + style.padding.y + headlineSize.y + style.lineGap), + style.descriptionColor, + data.description.c_str()); + if (hasDetail) + { + drawList.AddText( + ImVec2(overlayPos.x + style.padding.x, overlayPos.y + style.padding.y + headlineSize.y + descriptionSize.y + style.lineGap * 2.0f), + style.detailColor, + data.detail.c_str()); + } + } + + static inline void beginHoverInfoOverlay(const char* name, const ImVec2& mousePos, const SCameraHoverInfoOverlayStyle& style = {}) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, style.windowBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_Border, style.borderColor); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, style.borderSize); + ImGui::SetNextWindowPos(ImVec2(mousePos.x + style.mouseOffset.x, mousePos.y + style.mouseOffset.y), ImGuiCond_Always); + ImGui::Begin(name, nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); + } + + static inline void endHoverInfoOverlay() + { + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + + static inline void drawViewportSplitOverlay( + ImDrawList& drawList, + const ImVec2& displaySize, + const float splitY, + const float gap, + const SCameraViewportSplitOverlayStyle& style = {}) + { + if (gap >= style.minimumGapFill) + { + drawList.AddRectFilled( + ImVec2(0.0f, splitY), + ImVec2(displaySize.x, splitY + gap), + style.gapFillColor); + return; + } + + drawList.AddLine( + ImVec2(0.0f, splitY), + ImVec2(displaySize.x, splitY), + style.dividerLineColor, + style.dividerLineThickness); + } + + static inline void drawFollowTargetViewportOverlay( + ImDrawList& drawList, + const system::SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + const SViewportOverlayRect& viewportRect, + const SCameraFollowTargetViewportOverlayStyle& style = {}) + { + if (!viewportRect.valid()) + return; + + system::SCameraProjectedTargetMetrics projectedTarget = {}; + if (!system::CCameraFollowRegressionUtilities::tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, projectedTarget)) + return; + + const bool centered = projectedTarget.radius <= style.centeredNdcRadius; + const ImVec2 center = viewportRect.getCenter(); + const ImVec2 target = viewportRect.ndcToScreen(ImVec2(projectedTarget.ndc.x, projectedTarget.ndc.y)); + const float targetRadius = centered ? style.centeredTargetRadius : style.defaultTargetRadius; + const ImU32 targetColor = centered ? style.centeredTargetColor : style.defaultTargetColor; + const ImU32 targetFillColor = centered ? style.centeredTargetFillColor : style.defaultTargetFillColor; + const ImU32 linkColor = centered ? style.centeredLinkColor : style.defaultLinkColor; + + drawList.AddCircle(center, style.centerRadius, style.centerColor, style.circleSegments, style.centerCircleThickness); + drawList.AddLine( + ImVec2(center.x - style.centerCrossHalfExtent, center.y), + ImVec2(center.x + style.centerCrossHalfExtent, center.y), + style.centerColor, + style.centerLineThickness); + drawList.AddLine( + ImVec2(center.x, center.y - style.centerCrossHalfExtent), + ImVec2(center.x, center.y + style.centerCrossHalfExtent), + style.centerColor, + style.centerLineThickness); + + drawList.AddLine(center, target, linkColor, style.linkLineThickness); + drawList.AddCircleFilled(target, targetRadius, targetFillColor, style.filledCircleSegments); + drawList.AddCircle(target, targetRadius, targetColor, style.circleSegments, style.centerCircleThickness); + drawList.AddLine( + ImVec2(target.x - style.targetCrossHalfExtent, target.y), + ImVec2(target.x + style.targetCrossHalfExtent, target.y), + targetColor, + style.centerLineThickness); + drawList.AddLine( + ImVec2(target.x, target.y - style.targetCrossHalfExtent), + ImVec2(target.x, target.y + style.targetCrossHalfExtent), + targetColor, + style.centerLineThickness); + + drawList.AddText( + ImVec2(target.x + style.labelOffsetX, target.y + style.labelOffsetY), + targetColor, + "FOLLOW TARGET"); + } +}; + +} // namespace nbl::ui + +#endif // _C_CAMERA_VIEWPORT_OVERLAY_UTILITIES_HPP_ diff --git a/common/include/nbl/examples/PCH.hpp b/common/include/nbl/examples/PCH.hpp index a20984464..c31221945 100644 --- a/common/include/nbl/examples/PCH.hpp +++ b/common/include/nbl/examples/PCH.hpp @@ -20,10 +20,8 @@ #include "nbl/examples/common/InputSystem.hpp" #include "nbl/examples/common/CEventCallback.hpp" -#include "nbl/examples/cameras/CCamera.hpp" - #include "nbl/examples/geometry/CGeometryCreatorScene.hpp" #include "nbl/examples/geometry/CSimpleDebugRenderer.hpp" -#endif // _NBL_EXAMPLES_COMMON_PCH_HPP_ \ No newline at end of file +#endif // _NBL_EXAMPLES_COMMON_PCH_HPP_ diff --git a/common/include/nbl/examples/cameras/CCamera.hpp b/common/include/nbl/examples/cameras/CCamera.hpp deleted file mode 100644 index f185e60f6..000000000 --- a/common/include/nbl/examples/cameras/CCamera.hpp +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h -#ifndef _NBL_COMMON_CAMERA_IMPL_ -#define _NBL_COMMON_CAMERA_IMPL_ - - -#include - -#include -#include -#include -#include - -#include -#include -#include - -class Camera -{ -public: - Camera() = default; - Camera(const nbl::core::vectorSIMDf& position, const nbl::core::vectorSIMDf& lookat, const nbl::hlsl::float32_t4x4& projection, float moveSpeed = 1.0f, float rotateSpeed = 1.0f, const nbl::core::vectorSIMDf& upVec = nbl::core::vectorSIMDf(0.0f, 1.0f, 0.0f), const nbl::core::vectorSIMDf& backupUpVec = nbl::core::vectorSIMDf(0.5f, 1.0f, 0.0f)) - : position(position) - , initialPosition(position) - , target(lookat) - , initialTarget(lookat) - , firstUpdate(true) - , moveSpeed(moveSpeed) - , rotateSpeed(rotateSpeed) - , upVector(upVec) - , backupUpVector(backupUpVec) - , viewMatrix(nbl::hlsl::math::linalg::diagonal(1.0f)) - { - initDefaultKeysMap(); - allKeysUp(); - setProjectionMatrix(projection); - recomputeViewMatrix(); - } - - ~Camera() = default; - - enum E_CAMERA_MOVE_KEYS : uint8_t - { - ECMK_MOVE_FORWARD = 0, - ECMK_MOVE_BACKWARD, - ECMK_MOVE_LEFT, - ECMK_MOVE_RIGHT, - ECMK_COUNT, - }; - - inline void mapKeysToWASD() - { - keysMap[ECMK_MOVE_FORWARD] = nbl::ui::EKC_W; - keysMap[ECMK_MOVE_BACKWARD] = nbl::ui::EKC_S; - keysMap[ECMK_MOVE_LEFT] = nbl::ui::EKC_A; - keysMap[ECMK_MOVE_RIGHT] = nbl::ui::EKC_D; - } - - inline void mapKeysToArrows() - { - keysMap[ECMK_MOVE_FORWARD] = nbl::ui::EKC_UP_ARROW; - keysMap[ECMK_MOVE_BACKWARD] = nbl::ui::EKC_DOWN_ARROW; - keysMap[ECMK_MOVE_LEFT] = nbl::ui::EKC_LEFT_ARROW; - keysMap[ECMK_MOVE_RIGHT] = nbl::ui::EKC_RIGHT_ARROW; - } - - inline void mapKeysCustom(std::array& map) { keysMap = map; } - - inline const nbl::hlsl::float32_t4x4& getProjectionMatrix() const { return projMatrix; } - inline const nbl::hlsl::float32_t3x4& getViewMatrix() const { return viewMatrix; } - inline const nbl::hlsl::float32_t4x4& getConcatenatedMatrix() const { return concatMatrix; } - - inline void setProjectionMatrix(const nbl::hlsl::float32_t4x4& projection) - { - projMatrix = projection; - leftHanded = nbl::hlsl::determinant(projMatrix) < 0.f; - concatMatrix = nbl::hlsl::math::linalg::promoted_mul(projMatrix, viewMatrix); - } - - inline void setPosition(const nbl::core::vectorSIMDf& pos) - { - position.set(pos); - recomputeViewMatrix(); - } - - inline const nbl::core::vectorSIMDf& getPosition() const { return position; } - - inline void setTarget(const nbl::core::vectorSIMDf& pos) - { - target.set(pos); - recomputeViewMatrix(); - } - - inline const nbl::core::vectorSIMDf& getTarget() const { return target; } - - inline void setUpVector(const nbl::core::vectorSIMDf& up) { upVector = up; } - - inline void setBackupUpVector(const nbl::core::vectorSIMDf& up) { backupUpVector = up; } - - inline const nbl::core::vectorSIMDf& getUpVector() const { return upVector; } - - inline const nbl::core::vectorSIMDf& getBackupUpVector() const { return backupUpVector; } - - inline const float getMoveSpeed() const { return moveSpeed; } - - inline void setMoveSpeed(const float _moveSpeed) { moveSpeed = _moveSpeed; } - - inline const float getRotateSpeed() const { return rotateSpeed; } - - inline void setRotateSpeed(const float _rotateSpeed) { rotateSpeed = _rotateSpeed; } - - inline void recomputeViewMatrix() - { - nbl::hlsl::float32_t3 pos = nbl::core::convertToHLSLVector(position).xyz; - nbl::hlsl::float32_t3 localTarget = nbl::hlsl::normalize(nbl::core::convertToHLSLVector(target).xyz - pos); - // TODO: remove completely when removing vectorSIMD - nbl::hlsl::float32_t3 _target = nbl::core::convertToHLSLVector(target).xyz; - - // if upvector and vector to the target are the same, we have a - // problem. so solve this problem: - nbl::hlsl::float32_t3 up = nbl::core::convertToHLSLVector(nbl::core::normalize(upVector)).xyz; - nbl::hlsl::float32_t3 cross = nbl::hlsl::cross(localTarget, up); - const float squaredLength = dot(cross, cross); - const bool upVectorNeedsChange = squaredLength == 0; - if (upVectorNeedsChange) - up = nbl::core::convertToHLSLVector(nbl::core::normalize(backupUpVector)); - - if (leftHanded) - viewMatrix = nbl::hlsl::math::linalg::lhLookAt(pos, _target, up); - else - viewMatrix = nbl::hlsl::math::linalg::rhLookAt(pos, _target, up); - - concatMatrix = nbl::hlsl::math::linalg::promoted_mul(projMatrix, viewMatrix); - } - - inline bool getLeftHanded() const { return leftHanded; } - -public: - - void mouseProcess(const nbl::ui::IMouseEventChannel::range_t& events) - { - for (auto eventIt=events.begin(); eventIt!=events.end(); eventIt++) - { - auto ev = *eventIt; - - if(ev.type == nbl::ui::SMouseEvent::EET_CLICK && ev.clickEvent.mouseButton == nbl::ui::EMB_LEFT_BUTTON) - if(ev.clickEvent.action == nbl::ui::SMouseEvent::SClickEvent::EA_PRESSED) - mouseDown = true; - else if (ev.clickEvent.action == nbl::ui::SMouseEvent::SClickEvent::EA_RELEASED) - mouseDown = false; - - if(ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT && mouseDown) - { - nbl::hlsl::float32_t4 pos = nbl::core::convertToHLSLVector(getPosition()); - nbl::hlsl::float32_t4 localTarget = nbl::core::convertToHLSLVector(getTarget()) - pos; - - // Get Relative Rotation for localTarget in Radians - float relativeRotationX, relativeRotationY; - relativeRotationY = atan2(localTarget.x, localTarget.z); - const double z1 = nbl::core::sqrt(localTarget.x*localTarget.x + localTarget.z*localTarget.z); - relativeRotationX = atan2(z1, localTarget.y) - nbl::core::PI()/2; - - constexpr float RotateSpeedScale = 0.003f; - relativeRotationX -= ev.movementEvent.relativeMovementY * rotateSpeed * RotateSpeedScale * -1.0f; - float tmpYRot = ev.movementEvent.relativeMovementX * rotateSpeed * RotateSpeedScale * -1.0f; - - if (leftHanded) - relativeRotationY -= tmpYRot; - else - relativeRotationY += tmpYRot; - - const double MaxVerticalAngle = nbl::core::radians(88.0f); - - if (relativeRotationX > MaxVerticalAngle*2 && relativeRotationX < 2 * nbl::core::PI()-MaxVerticalAngle) - relativeRotationX = 2 * nbl::core::PI()-MaxVerticalAngle; - else - if (relativeRotationX > MaxVerticalAngle && relativeRotationX < 2 * nbl::core::PI()-MaxVerticalAngle) - relativeRotationX = MaxVerticalAngle; - - pos.w = 0; - localTarget = nbl::hlsl::float32_t4(0, 0, nbl::core::max(1.f, nbl::hlsl::length(pos)), 1.0f); - - const nbl::hlsl::math::quaternion quat = nbl::hlsl::math::quaternion::create(relativeRotationX, relativeRotationY, 0.0f); - nbl::hlsl::float32_t3x4 mat = nbl::hlsl::math::linalg::promote_affine<3, 4, 3, 3>(quat.__constructMatrix()); - - - localTarget = nbl::hlsl::float32_t4(nbl::hlsl::mul(mat, localTarget), 1.0f); - - nbl::core::vectorSIMDf finalTarget = nbl::core::constructVecorSIMDFromHLSLVector(localTarget + pos); - finalTarget.w = 1.0f; - setTarget(finalTarget); - } - } - } - - void keyboardProcess(const nbl::ui::IKeyboardEventChannel::range_t& events) - { - for(uint32_t k = 0; k < E_CAMERA_MOVE_KEYS::ECMK_COUNT; ++k) - perActionDt[k] = 0.0; - - /* - * If a Key was already being held down from previous frames - * Compute with this assumption that the key will be held down for this whole frame as well, - * And If an UP event was sent It will get subtracted it from this value. (Currently Disabled Because we Need better Oracle) - */ - - for(uint32_t k = 0; k < E_CAMERA_MOVE_KEYS::ECMK_COUNT; ++k) - if(keysDown[k]) - { - auto timeDiff = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - if (timeDiff < 0) - timeDiff = 0; - perActionDt[k] += timeDiff; - } - - for (auto eventIt=events.begin(); eventIt!=events.end(); eventIt++) - { - const auto ev = *eventIt; - - // accumulate the periods for which a key was down - auto timeDiff = std::chrono::duration_cast(nextPresentationTimeStamp - ev.timeStamp).count(); - if (timeDiff < 0) - timeDiff = 0; - - // handle camera movement - for (const auto logicalKey : { ECMK_MOVE_FORWARD, ECMK_MOVE_BACKWARD, ECMK_MOVE_LEFT, ECMK_MOVE_RIGHT }) - { - const auto code = keysMap[logicalKey]; - - if (ev.keyCode == code) - { - if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !keysDown[logicalKey]) - { - perActionDt[logicalKey] += timeDiff; - keysDown[logicalKey] = true; - } - else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - { - // perActionDt[logicalKey] -= timeDiff; - keysDown[logicalKey] = false; - } - } - } - - // handle reset to default state - if (ev.keyCode == nbl::ui::EKC_HOME) - if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - { - position = initialPosition; - target = initialTarget; - recomputeViewMatrix(); - } - } - } - - void beginInputProcessing(std::chrono::microseconds _nextPresentationTimeStamp) - { - nextPresentationTimeStamp = _nextPresentationTimeStamp; - return; - } - - void endInputProcessing(std::chrono::microseconds _nextPresentationTimeStamp) - { - nbl::core::vectorSIMDf pos = getPosition(); - nbl::core::vectorSIMDf localTarget = getTarget() - pos; - - if (!firstUpdate) - { - nbl::core::vectorSIMDf movedir = localTarget; - movedir.makeSafe3D(); - movedir = nbl::core::normalize(movedir); - - constexpr float MoveSpeedScale = 0.02f; - - pos += movedir * perActionDt[E_CAMERA_MOVE_KEYS::ECMK_MOVE_FORWARD] * moveSpeed * MoveSpeedScale; - pos -= movedir * perActionDt[E_CAMERA_MOVE_KEYS::ECMK_MOVE_BACKWARD] * moveSpeed * MoveSpeedScale; - - // strafing - - // if upvector and vector to the target are the same, we have a - // problem. so solve this problem: - nbl::core::vectorSIMDf up = nbl::core::normalize(upVector); - nbl::core::vectorSIMDf cross = nbl::core::cross(localTarget, up); - bool upVectorNeedsChange = nbl::core::lengthsquared(cross)[0] == 0; - if (upVectorNeedsChange) - { - up = nbl::core::normalize(backupUpVector); - } - - nbl::core::vectorSIMDf strafevect = localTarget; - if (leftHanded) - strafevect = nbl::core::cross(strafevect, up); - else - strafevect = nbl::core::cross(up, strafevect); - - strafevect = nbl::core::normalize(strafevect); - - pos += strafevect * perActionDt[E_CAMERA_MOVE_KEYS::ECMK_MOVE_LEFT] * moveSpeed * MoveSpeedScale; - pos -= strafevect * perActionDt[E_CAMERA_MOVE_KEYS::ECMK_MOVE_RIGHT] * moveSpeed * MoveSpeedScale; - } - else - firstUpdate = false; - - setPosition(pos); - setTarget(localTarget+pos); - - lastVirtualUpTimeStamp = nextPresentationTimeStamp; - } - -private: - - inline void initDefaultKeysMap() { mapKeysToWASD(); } - - inline void allKeysUp() - { - for (uint32_t i=0; i< E_CAMERA_MOVE_KEYS::ECMK_COUNT; ++i) - keysDown[i] = false; - - mouseDown = false; - } - -private: - nbl::core::vectorSIMDf initialPosition, initialTarget, position, target, upVector, backupUpVector; // TODO: make first 2 const + add default copy constructor - nbl::hlsl::float32_t3x4 viewMatrix; - nbl::hlsl::float32_t4x4 concatMatrix, projMatrix; - - float moveSpeed, rotateSpeed; - bool leftHanded, firstUpdate = true, mouseDown = false; - - std::array keysMap = { {nbl::ui::EKC_NONE} }; // map camera E_CAMERA_MOVE_KEYS to corresponding Nabla key codes, by default camera uses WSAD to move - // TODO: make them use std::array - bool keysDown[E_CAMERA_MOVE_KEYS::ECMK_COUNT] = {}; - double perActionDt[E_CAMERA_MOVE_KEYS::ECMK_COUNT] = {}; // durations for which the key was being held down from lastVirtualUpTimeStamp(=last "guessed" presentation time) to nextPresentationTimeStamp - - std::chrono::microseconds nextPresentationTimeStamp, lastVirtualUpTimeStamp; -}; -#endif diff --git a/common/include/nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp b/common/include/nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp new file mode 100644 index 000000000..2f4d39d2a --- /dev/null +++ b/common/include/nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp @@ -0,0 +1,174 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h +#ifndef _NBL_EXAMPLES_CAMERAS_C_CAMERA_SIMPLE_FPS_UTILITIES_HPP_INCLUDED_ +#define _NBL_EXAMPLES_CAMERAS_C_CAMERA_SIMPLE_FPS_UTILITIES_HPP_INCLUDED_ + +#include +#include + +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" +#include "nbl/ext/Cameras/CFPSCamera.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" + +namespace nbl::examples +{ + +/// @brief Small example-side helpers for the most basic mouse+keyboard FPS usage. +/// +/// This helper exists only to reduce repeated boilerplate in simple examples. +/// It does not wrap or replace the `ext/Cameras` stack. +/// Advanced paths such as ImGuizmo-driven edits, world-space gizmo translation, +/// goal solving, scripted playback, follow runtime, or non-FPS camera families +/// should keep using the full camera API directly. +struct CCameraSimpleFPSUtilities final +{ + enum class EMouseLookMode : uint8_t + { + HoldButton, + AlwaysActive + }; + + /// @brief Mutable runtime state for the basic mouse+keyboard FPS input path. + /// + /// This groups the FPS input binder and the small amount of state needed for + /// button-gated mouse look. + /// The binder is referenced through a non-owning pointer so examples keep full + /// ownership of binding setup and rebinding policy. + struct SBasicInputRuntime final + { + ui::CGimbalInputBinder* binder = nullptr; + bool lookActive = false; + }; + + /// @brief Read-only configuration for the basic mouse+keyboard FPS input path. + struct SBasicInputConfig final + { + EMouseLookMode lookMode = EMouseLookMode::HoldButton; + ui::E_MOUSE_BUTTON lookButton = ui::EMB_LEFT_BUTTON; + }; + + /// @brief Example-facing FPS speed knobs matching the removed legacy wrapper. + /// + /// `moveSpeed` and `rotationSpeed` are the same user-level values that the + /// old example camera wrapper exposed. They are mapped to camera-local scales + /// with the exact same numerical factors as before, so existing example + /// values preserve the same motion semantics. + struct SSpeedSettings final + { + double moveSpeed = 1.0; + double rotationSpeed = 1.0; + }; + + /// @brief Create a normal `CFPSCamera` from a look-at pair and simple motion scales. + /// + /// Returns `nullptr` when the look-at orientation cannot be resolved from the + /// provided position, target, and preferred up-vector. + static inline core::smart_refctd_ptr createFromLookAt( + const hlsl::float64_t3& position, + const hlsl::float64_t3& target, + const double moveSpeedScale, + const double rotationSpeedScale, + const hlsl::float64_t3& preferredUp = hlsl::float64_t3(0.0, 1.0, 0.0)) + { + hlsl::camera_quaternion_t orientation; + if (!hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, target, preferredUp, orientation)) + return nullptr; + + auto camera = core::make_smart_refctd_ptr(position, orientation); + if (!camera) + return nullptr; + camera->setMoveSpeedScale(moveSpeedScale); + camera->setRotationSpeedScale(rotationSpeedScale); + return camera; + } + + /// @brief Apply example-facing speed settings using the exact old wrapper mapping. + static inline void applySpeedSettings( + core::CFPSCamera& camera, + const SSpeedSettings& speedSettings) + { + camera.setMoveSpeedScale(toMoveSpeedScale(speedSettings.moveSpeed)); + camera.setRotationSpeedScale(toRotationSpeedScale(speedSettings.rotationSpeed)); + } + + /// @brief Create a normal `CFPSCamera` from a look-at pair using the same speed values as the old wrapper. + static inline core::smart_refctd_ptr createFromLookAt( + const hlsl::float64_t3& position, + const hlsl::float64_t3& target, + const SSpeedSettings& speedSettings, + const hlsl::float64_t3& preferredUp = hlsl::float64_t3(0.0, 1.0, 0.0)) + { + return createFromLookAt( + position, + target, + toMoveSpeedScale(speedSettings.moveSpeed), + toRotationSpeedScale(speedSettings.rotationSpeed), + preferredUp); + } + + /// @brief Collect virtual FPS events from already-consumed mouse and keyboard events. + /// + /// This path covers only the common `mouse + keyboard + button-gated look` + /// setup used by simple examples. + /// The caller keeps ownership of channel consumption and may reuse the same + /// raw event spans for other example-local logic before or after this helper. + static inline std::vector collectBasicVirtualEvents( + const std::span mouseEvents, + const std::span keyboardEvents, + const std::chrono::microseconds nextPresentationTimestamp, + SBasicInputRuntime& runtime, + const SBasicInputConfig& config = {}) + { + if (!runtime.binder) + return {}; + + std::vector cameraMouseEvents; + + for (const auto& event : mouseEvents) + { + if (config.lookMode == EMouseLookMode::HoldButton && + event.type == ui::SMouseEvent::EET_CLICK && + event.clickEvent.mouseButton == config.lookButton) + { + if (event.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + runtime.lookActive = true; + else if (event.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + runtime.lookActive = false; + } + + const bool allowLook = (config.lookMode == EMouseLookMode::AlwaysActive) || runtime.lookActive; + if (event.type == ui::SMouseEvent::EET_MOVEMENT && allowLook) + cameraMouseEvents.push_back(event); + } + + auto collected = runtime.binder->collectVirtualEvents( + nextPresentationTimestamp, + { + .keyboardEvents = keyboardEvents, + .mouseEvents = { cameraMouseEvents.data(), cameraMouseEvents.size() } + }); + + return std::move(collected.events); + } + +private: + // These are the exact same factors that the removed legacy example camera wrapper used. + static inline constexpr double MoveSpeedScaleMultiplier = 2.0; + static inline constexpr double RotationSpeedScaleMultiplier = 0.003; + + static inline double toMoveSpeedScale(const double moveSpeed) + { + return moveSpeed * MoveSpeedScaleMultiplier; + } + + static inline double toRotationSpeedScale(const double rotationSpeed) + { + return rotationSpeed * RotationSpeedScaleMultiplier; + } +}; + +} // namespace nbl::examples + +#endif // _NBL_EXAMPLES_CAMERAS_C_CAMERA_SIMPLE_FPS_UTILITIES_HPP_INCLUDED_ diff --git a/common/include/nbl/examples/common/CEventCallback.hpp b/common/include/nbl/examples/common/CEventCallback.hpp index cae6dc7de..b45eb5841 100644 --- a/common/include/nbl/examples/common/CEventCallback.hpp +++ b/common/include/nbl/examples/common/CEventCallback.hpp @@ -2,14 +2,14 @@ #define _NBL_EXAMPLES_COMMON_C_EVENT_CALLBACK_HPP_INCLUDED_ -#include "nbl/video/utilities/CSimpleResizeSurface.h" +#include "nbl/video/utilities/CSmoothResizeSurface.h" #include "nbl/examples/common/InputSystem.hpp" namespace nbl::examples { -class CEventCallback : public nbl::video::ISimpleManagedSurface::ICallback +class CEventCallback : public nbl::video::ISmoothResizeSurface::ICallback { public: CEventCallback(nbl::core::smart_refctd_ptr&& m_inputSystem, nbl::system::logger_opt_smart_ptr&& logger) : m_inputSystem(std::move(m_inputSystem)), m_logger(std::move(logger)) {} @@ -51,4 +51,4 @@ class CEventCallback : public nbl::video::ISimpleManagedSurface::ICallback nbl::system::logger_opt_smart_ptr m_logger = nullptr; }; } -#endif \ No newline at end of file +#endif diff --git a/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp b/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp index 6e5c24614..0f3daa6c9 100644 --- a/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp +++ b/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp @@ -35,7 +35,7 @@ class CSimpleDebugRenderer final : public core::IReferenceCounted hlsl::examples::geometry_creator_scene::SInstanceMatrices retval = { .worldViewProj = float32_t4x4(math::linalg::promoted_mul(float64_t4x4(viewProj),float64_t3x4(world))) }; - const auto sub3x3 = mul(float64_t3x3(viewProj),float64_t3x3(world)); + const auto sub3x3 = mul(float64_t3x3(view),float64_t3x3(world)); retval.normal = float32_t3x3(transpose(inverse(sub3x3))); return retval; } diff --git a/common/src/nbl/examples/shaders/geometry/unified.hlsl b/common/src/nbl/examples/shaders/geometry/unified.hlsl index 40c90204d..7f0d0d7cb 100644 --- a/common/src/nbl/examples/shaders/geometry/unified.hlsl +++ b/common/src/nbl/examples/shaders/geometry/unified.hlsl @@ -14,6 +14,7 @@ struct SInterpolants { float32_t4 ndc : SV_Position; float32_t3 meta : COLOR1; + float32_t2 gridUV : COLOR2; }; #include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" @@ -38,6 +39,7 @@ SInterpolants BasicVS(uint32_t VertexIndex : SV_VertexID) output.meta = mul(pc.matrices.normal,utbs[pc.normalView][VertexIndex].xyz); else output.meta = mul(inverse(transpose(pc.matrices.normal)),position); + output.gridUV = position.xz; return output; } [shader("pixel")] @@ -50,17 +52,30 @@ float32_t4 BasicFS(SInterpolants input) : SV_Target0 // Debug fragment shader for grid triangle-strips ("snake" order). It alternates // triangle shading to visualize strip winding and connectivity. [shader("pixel")] -float32_t4 BasicFSSnake(SInterpolants input, uint primID : SV_PrimitiveID) : SV_Target0 +float32_t4 BasicFSSnake(SInterpolants input) : SV_Target0 { - float3 N = normalize(pc.normalView < SPushConstants::DescriptorCount ? input.meta : reconstructGeometricNormal(input.meta)); - float3 base = (primID & 1u) ? float3(0.68,0.68,0.68) : float3(0.88,0.88,0.88); + float2 uv = input.gridUV * 32.0; + float2 edge = min(frac(uv), 1.0 - frac(uv)); + float2 aa = max(fwidth(uv), 1e-4.xx); - float nview = saturate(0.5 + 0.5 * N.z); - float grad = pow(nview, 0.5); - float rim = pow(1.0 - nview, 2.0) * 0.25; + float minorX = 1.0 - smoothstep(0.0, aa.x * 1.6, edge.x); + float minorY = 1.0 - smoothstep(0.0, aa.y * 1.6, edge.y); + float minor = max(minorX, minorY); - float3 col = base * (0.2 + 0.8 * grad) + rim; - return float4(col, 1.0); + float2 uvMajor = uv * 0.25; + float2 edgeMajor = min(frac(uvMajor), 1.0 - frac(uvMajor)); + float majorX = 1.0 - smoothstep(0.0, aa.x * 0.55, edgeMajor.x); + float majorY = 1.0 - smoothstep(0.0, aa.y * 0.55, edgeMajor.y); + float major = max(majorX, majorY); + + float lineMask = max(minor * 0.70, major); + if (lineMask < 0.03) + discard; + + float3 colMinor = float3(0.58, 0.66, 0.78); + float3 colMajor = float3(0.76, 0.83, 0.92); + float3 color = lerp(colMinor, colMajor, saturate(major)); + return float4(color, 1.0); } // TODO: do smooth normals on the cone @@ -72,6 +87,7 @@ SInterpolants ConeVS(uint32_t VertexIndex : SV_VertexID) SInterpolants output; output.ndc = math::linalg::promoted_mul(pc.matrices.worldViewProj,position); output.meta = mul(inverse(transpose(pc.matrices.normal)),position); + output.gridUV = position.xz; return output; } [shader("pixel")] diff --git a/manifests/envmaps/space_spheremaps/LICENSE.txt.dvc b/manifests/envmaps/space_spheremaps/LICENSE.txt.dvc new file mode 100644 index 000000000..b2a5fad4e --- /dev/null +++ b/manifests/envmaps/space_spheremaps/LICENSE.txt.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4f51b958b13a5aa10ade7435099e774b + size: 732 + hash: md5 + path: LICENSE.txt diff --git a/manifests/envmaps/space_spheremaps/rich_blue_nebulae_1_8k.rgba16f.envblob.dvc b/manifests/envmaps/space_spheremaps/rich_blue_nebulae_1_8k.rgba16f.envblob.dvc new file mode 100644 index 000000000..d34548748 --- /dev/null +++ b/manifests/envmaps/space_spheremaps/rich_blue_nebulae_1_8k.rgba16f.envblob.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 308a9569b32cf051867f9c9483209e33 + size: 268435480 + hash: md5 + path: rich_blue_nebulae_1_8k.rgba16f.envblob